Index: main/waeup.kofa/tags/0.2/CHANGES.txt
===================================================================
--- main/waeup.kofa/tags/0.2/CHANGES.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/CHANGES.txt	(revision 10886)
@@ -0,0 +1,7 @@
+Changes
+*******
+
+0.1 (unreleased)
+================
+
+Initial version. Copy from waeup.sirp.
Index: main/waeup.kofa/tags/0.2/LICENSE
===================================================================
--- main/waeup.kofa/tags/0.2/LICENSE	(revision 10886)
+++ main/waeup.kofa/tags/0.2/LICENSE	(revision 10886)
@@ -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/tags/0.2/README-zeo.txt
===================================================================
--- main/waeup.kofa/tags/0.2/README-zeo.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/README-zeo.txt	(revision 10886)
@@ -0,0 +1,156 @@
+Deploying waeup.kofa as 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 waeup.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 (paramters, 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.
+
+Creating new clients
+====================
+
+You want more 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.
+
Index: main/waeup.kofa/tags/0.2/README.txt
===================================================================
--- main/waeup.kofa/tags/0.2/README.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/README.txt	(revision 10886)
@@ -0,0 +1,14 @@
+What is WAeUP.Kofa ?
+********************
+
+Kofa, which means 'entrance' or 'portal' in Hausa language,
+is a multifunctional Student Management System and a
+web portal to transparent and comprehensive information about
+study progress. The system controls application and student
+registration processes at large schools and universities in Nigeria.
+
+Kofa is the flagship of the West African eUniversity Project (WAeUP).
+See http://www.waeup.org to learn more about WAeUP.
+
+Installation instructions can be found in the User Documentation (docs
+folder in the file system).
Index: main/waeup.kofa/tags/0.2/bootstrap.py
===================================================================
--- main/waeup.kofa/tags/0.2/bootstrap.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/bootstrap.py	(revision 10886)
@@ -0,0 +1,165 @@
+##############################################################################
+#
+# 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, shutil, sys, tempfile
+from optparse import OptionParser
+
+tmpeggs = tempfile.mkdtemp()
+
+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 --setup-source and --download-base to point to
+local resources, you can keep this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("-v", "--version", help="use a specific zc.buildout 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"))
+
+
+options, args = parser.parse_args()
+
+######################################################################
+# load/install distribute
+
+to_reload = False
+try:
+    import pkg_resources, setuptools
+    if not hasattr(pkg_resources, '_distribute'):
+        to_reload = True
+        raise ImportError
+except ImportError:
+    ez = {}
+
+    try:
+        from urllib.request import urlopen
+    except ImportError:
+        from urllib2 import urlopen
+
+    exec(urlopen('http://python-distribute.org/distribute_setup.py').read(), ez)
+    setup_args = dict(to_dir=tmpeggs, download_delay=0, no_fake=True)
+    ez['use_setuptools'](**setup_args)
+
+    if to_reload:
+        reload(pkg_resources)
+    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
+
+cmd = [sys.executable, '-c',
+       '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])
+
+distribute_path = ws.find(
+    pkg_resources.Requirement.parse('distribute')).location
+
+requirement = 'zc.buildout'
+version = options.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):
+        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=[distribute_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, env=dict(os.environ, PYTHONPATH=distribute_path)) != 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/tags/0.2/buildout-windows.cfg
===================================================================
--- main/waeup.kofa/tags/0.2/buildout-windows.cfg	(revision 10886)
+++ main/waeup.kofa/tags/0.2/buildout-windows.cfg	(revision 10886)
@@ -0,0 +1,87 @@
+[buildout]
+develop = .
+parts =
+    eggbasket
+    app
+    mkdirs
+    zope_conf
+    site_zcml
+    zdaemon_conf
+    deploy_ini
+    debug_ini
+# 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
+
+# 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.
+
+[app]
+# This creates only the paster script in bin/.
+recipe = zc.recipe.egg
+eggs = waeup.kofa
+       z3c.evalexception>=2.0
+       Paste
+       PasteScript
+       PasteDeploy
+interpreter = python-console
+scripts = paster=paster
+
+[mkdirs]
+recipe = z3c.recipe.mkdir
+paths =
+    ${zope_conf:filestorage}
+    ${zope_conf:logfiles}
+    ${zope_conf:blobstorage}
+    ${buildout:directory}/var/datacenter/media
+
+[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
+
+[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
+
+[deploy_ini]
+recipe = collective.recipe.template
+input = etc/deploy.ini.in
+output = ${buildout:parts-directory}/etc/deploy.ini
+
+[debug_ini]
+recipe = collective.recipe.template
+input = etc/debug.ini.in
+output = ${buildout:parts-directory}/etc/debug.ini
+
+[eggbasket]
+recipe = z3c.recipe.eggbasket
+eggs = grok
+url = http://grok.zope.org/releaseinfo/grok-eggs-1.1.tgz
Index: main/waeup.kofa/tags/0.2/buildout-zeo.cfg
===================================================================
--- main/waeup.kofa/tags/0.2/buildout-zeo.cfg	(revision 10886)
+++ main/waeup.kofa/tags/0.2/buildout-zeo.cfg	(revision 10886)
@@ -0,0 +1,127 @@
+# 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
+    deploy_ini
+    debug_ini
+    profile_ini
+    zeo1_ini
+    zeo2_ini
+    waeupdocs
+    coverage-detect
+    coverage-report
+# kofactl creation must be _after_ app!
+    kofactl
+# 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/zeo1.ini.in
+output = ${buildout:parts-directory}/etc/zeo1.ini
+
+[zeo2_ini]
+recipe = collective.recipe.template
+input = etc/zeo2.ini.in
+output = ${buildout:parts-directory}/etc/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]
+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]
+arguments = "${buildout:parts-directory}/etc/zeo2.conf"
+scripts = kofactl=zeo_client2
+
Index: main/waeup.kofa/tags/0.2/buildout.cfg
===================================================================
--- main/waeup.kofa/tags/0.2/buildout.cfg	(revision 10886)
+++ main/waeup.kofa/tags/0.2/buildout.cfg	(revision 10886)
@@ -0,0 +1,184 @@
+[buildout]
+develop = .
+parts =
+    eggbasket
+    app
+    i18n
+    test
+    mkdirs
+    zpasswd
+    zope_conf
+    site_zcml
+    zdaemon_conf
+    deploy_ini
+    debug_ini
+    profile_ini
+    waeupdocs
+    coverage-detect
+    coverage-report
+# kofactl creation must be _after_ app!
+    kofactl
+# 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
+
+# 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.
+
+[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]
+       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]
+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
+
+[deploy_ini]
+recipe = collective.recipe.template
+input = etc/deploy.ini.in
+output = ${buildout:parts-directory}/etc/deploy.ini
+
+[debug_ini]
+recipe = collective.recipe.template
+input = etc/debug.ini.in
+output = ${buildout:parts-directory}/etc/debug.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/working-directory/coverage', '${buildout:directory}/coverage-report')
Index: main/waeup.kofa/tags/0.2/docs/INSTALL.txt
===================================================================
--- main/waeup.kofa/tags/0.2/docs/INSTALL.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/INSTALL.txt	(revision 10886)
@@ -0,0 +1,84 @@
+Installation on Linux production system
+***************************************
+
+.. note:: `waeup.kofa` might not work with Python > 2.7
+          currently. Use of Python 2.6 or 2.7 is recommended.
+
+Prerequisites
+=============
+
+The `waeup.kofa` package is based on `Grok`_, which is a Python
+framework for agile webapplication development. Grok itself is based
+on `Zope`_.
+
+.. _Grok: http://grok.zope.org/
+
+.. _Zope: http://www.zope.org/
+
+Both, Grok and Zope, are written in Python (with parts written in
+C). You therefore need `Python`_ installed.
+
+.. _Python: http://www.python.org/
+
+Note, that you also need the Python header files and a compiler to
+compile the parts written in C.
+
+To deploy `waeup.kofa` most easily, we use `zc.buildout`_
+
+.. _zc.buildout: http://cheeseshop.python.org/pypi/zc.buildout
+
+You may have setuptools already installed for your system Python. In
+that case, you may need to upgrade it first because buildout requires
+a very recent version::
+
+    $ sudo easy_install -U setuptools
+
+If this command fails because easy_install is not available, there is
+a good chance you do not have setuptools available for your system
+Python. If so, there is no problem because setuptools will be
+installed locally by buildout.
+
+Bootstrap the buildout environment::
+
+    $ python2.6 bootstrap/bootstrap.py
+
+and run the buildout command::
+
+    $ bin/buildout
+    [lots of stuff will be downloaded and installed here]
+
+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
+
+Running the `waeup.kofa` application
+======================================
+
+You can start Zope with the with the following command::
+
+    $ bin/kofactl fg
+    
+Alternatively you can do:
+
+    $ bin/paster serve parts/etc/deploy.ini    
+
+If you now connect to port 8080 and log in with username 'grok',
+password 'grok', 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
+=================
+
+The tests are easily run by executing the test runner that's
+installed in the ``bin`` directory::
+
+    $ bin/test
+
Index: main/waeup.kofa/tags/0.2/docs/INSTALLWIN.txt
===================================================================
--- main/waeup.kofa/tags/0.2/docs/INSTALLWIN.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/INSTALLWIN.txt	(revision 10886)
@@ -0,0 +1,89 @@
+Installation on Windows (XP, 2000, Vista, 7)
+********************************************
+
+.. note:: `waeup.kofa` has been developed to run on Linux servers,
+          preferarbly with an Ubuntu OS. The installation on Windows computers
+          is for demonstration purposes only.
+
+Install Python
+==============
+
+**Step 1.** Install Python. Currently, version 2.6.x is supported.
+Note that these links point to installers for the ordinary x86 processor architecture.
+We recommend to download the follwoing software and install into ``C:\Python26``:
+
+| http://www.python.org/ftp/python/2.6.0/python-2.6.0.msi
+| http://sourceforge.net/project/downloading.php?group_id=78018&filename=pywin32-213.win32-py2.6.exe
+| http://pypi.python.org/packages/2.6/s/setuptools/setuptools-0.6c11.win32-py2.6.exe
+
+**Step 2**. Add ``C:\Python26\Scripts\`` to your system path:
+
+- Right-click on the My Computer icon. (Under Windows XP, Vista, or Windows 7, the My Computer Icon may be located in the start menu.)
+
+- Choose Properties from the context menu. Alternatively, you can double-click on the System icon in the Control Panel.
+
+- Click the Advanced tab or "Advanced system settings" on Windows Vista or Windows 7
+
+- Click the Environment Variables button.
+
+- Click on the variable called 'Path' and then click on Edit. Now add ``C:\Python26\Scripts\`` to the value of the variable, using a semi-colon as a separator.
+
+- Reboot Windows
+
+Install a Virtual Python Environment
+====================================
+
+**Step 3**. Open up a command window and change to the folder where you want to place the sandbox
+that will contain your Kofa project. In this HowTo we will assume and recommend this folder is ``C:\`` ::
+
+  C:\[any_path]> cd c:\
+
+**Step 4**. Install virtualenv by using the command::
+
+ C:\> easy_install virtualenv
+
+**Step 5**. Create an isolated environment (sandbox) using the command::
+
+ C:\> virtualenv winkofa
+
+**Step 6**. Activate the environment::
+
+ C:\> cd winkofa
+ C:\winkofa> Scripts\activate
+
+Note that the system prompt has changed.
+
+Run Kofa
+========
+
+**Step 7**. Copy the ``waeup.kofa`` folder into ``C:\winkofa``.
+
+**Step 8**. Run the bootstrap script::
+
+ (winkofa) C:\winkofa\waeup.kofa> cd waeup.kofa
+ (winkofa) C:\winkofa\waeup.kofa> python bootstrap\bootstrap.py -c buildout-windows.cfg
+
+**Step 9**. Run buildout (this takes a while)::
+
+ C:\winkofa\waeup.kofa> bin\buildout.exe -c buildout-windows.cfg
+
+**Step 10**. You can deactivate your environment again::
+
+ (winkofa) C:\winkofa\waeup.kofa> ..\Scripts\deactivate
+
+**Step 11**. Remove beaker from C:\winkofa\waeup.kofa\src\waeup\kofa\configure.zcml.
+
+**Step 12**. Start your Kofa::
+
+ C:\winkofa\waeup.kofa> bin\paster serve parts\etc\deploy.ini
+
+or from anywhere in your system::
+
+ C:\[any_path]> C:\winkofa\trunk\bin\paster serve C:\winkofa\trunk\parts\etc\deploy.ini
+
+**Step 13**. Visit the Kofa website and create your university instance:
+
+| http://localhost:8080
+| user name: grok
+| password: grok
+
Index: main/waeup.kofa/tags/0.2/docs/Makefile
===================================================================
--- main/waeup.kofa/tags/0.2/docs/Makefile	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/Makefile	(revision 10886)
@@ -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/tags/0.2/docs/make.bat
===================================================================
--- main/waeup.kofa/tags/0.2/docs/make.bat	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/make.bat	(revision 10886)
@@ -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/tags/0.2/docs/source/_templates/mainpage.template
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/_templates/mainpage.template	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/_templates/mainpage.template	(revision 10886)
@@ -0,0 +1,94 @@
+{% 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>
+
+<p><strong>Sections</strong></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">Get started with waeup.kofa.</span>
+    </p>
+
+    <p class="biglink">
+      <a class="biglink"
+         href="{{ pathto("userdocs/index") }}">User Documentation</a><br />
+    <span class="linkdescr">
+      Documentation for users and admins using waeup.kofa.
+    </span></p>
+
+    </td><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>
+
+    <p class="biglink">
+      <a class="biglink"
+         href="{{ pathto("developerdocs/index") }}">Developer Documentation</a><br />
+      <span class="linkdescr">
+        <a href="{{ pathto("developerdocs/api/index") }}">API</a> and other pieces
+        relevant for developers.
+      </span>
+    </p>
+
+  </td></tr>
+</table>
+
+<p><strong>Indices and tables</strong></p>
+<table class="contentstable" align="center"><tr>
+  <td width="50%">
+    <p class="biglink">
+      <a class="biglink"
+         href="{{ pathto("contents") }}">Table of Contents</a><br />
+      <span class="linkdescr">Lists all sections and subsections.</span>
+    </p>
+    </td><td width="50%">
+
+    <p class="biglink">
+      <a class="biglink"
+         href="{{ pathto("genindex") }}">Index</a><br />
+      <span class="linkdescr">All functions, classes, terms.</span>
+    </p>
+  </td></tr>
+</table>
+
+<p><strong>Further information</strong></p>
+<table class="contentstable" align="center"><tr>
+  <td width="50%">
+
+    <p class="biglink">
+      <a class="biglink"
+         href="{{ pathto("bugs") }}">Reporting Bugs</a><br />
+    </p>
+
+    <p class="biglink">
+      <a class="biglink"
+         href="{{ pathto("documentation") }}">About the Documentation</a><br />
+    </p>
+
+    </td><td width="50%">
+
+    <p class="biglink">
+      <a class="biglink"
+         href="{{ pathto("license") }}">License</a><br />
+    </p>
+
+    <p class="biglink">
+      <a class="biglink"
+         href="{{ pathto("copyright") }}">Copyright</a><br />
+    </p>
+
+    </td></tr>
+</table>
+
+
+{% endblock %}
Index: main/waeup.kofa/tags/0.2/docs/source/bugs.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/bugs.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/bugs.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/changes.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/changes.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/changes.rst	(revision 10886)
@@ -0,0 +1,1 @@
+.. include:: ../../CHANGES.txt
Index: main/waeup.kofa/tags/0.2/docs/source/conf.py
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/conf.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/conf.py	(revision 10886)
@@ -0,0 +1,213 @@
+# -*- 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 = 'default'
+
+# 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 = None
+
+# 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 = "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', 'WAeUPStudentInformationandRegistrationPortalKofa.tex', u'WAeUP Student Information and Registration Portal (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}
Index: main/waeup.kofa/tags/0.2/docs/source/contents.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/contents.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/contents.rst	(revision 10886)
@@ -0,0 +1,25 @@
+.. _contents:
+
+WAeUP.Kofa Documentation 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/tags/0.2/docs/source/copyright.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/copyright.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/copyright.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/developerdocs/api/accesscodes/api_accesscode.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/accesscodes/api_accesscode.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/accesscodes/api_accesscode.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`accesscodes.accesscode` -- AC Components
+*****************************************
+
+.. automodule:: waeup.kofa.accesscodes.accesscode
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/accesscodes/api_browser.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/accesscodes/api_browser.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/accesscodes/api_browser.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/developerdocs/api/accesscodes/api_catalog.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/accesscodes/api_catalog.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/accesscodes/api_catalog.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`accesscodes.catalog` -- AC Catalog Components
+**********************************************
+
+.. automodule:: waeup.kofa.accesscodes.catalog
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/accesscodes/api_interfaces.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/accesscodes/api_interfaces.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/accesscodes/api_interfaces.rst	(revision 10886)
@@ -0,0 +1,14 @@
+`accesscodes.interfaces` -- AC Interfaces
+*****************************************
+
+.. automodule:: waeup.kofa.accesscodes.interfaces
+   :members:
+   :undoc-members:
+
+
+   .. autointerface:: IAccessCode
+      :members:
+   .. autointerface:: IAccessCodeBatch
+      :members:
+   .. autointerface:: IAccessCodeBatchContainer
+      :members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/accesscodes/api_workflow.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/accesscodes/api_workflow.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/accesscodes/api_workflow.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`accesscodes.workflow` - AC Workflow Components
+***********************************************
+
+.. automodule:: waeup.kofa.accesscodes.workflow
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_accesscodes.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_accesscodes.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_accesscodes.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/developerdocs/api/api_app.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_app.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_app.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`app` -- University Root (Site) Module
+**************************************
+
+.. automodule:: waeup.kofa.app
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_applicants.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_applicants.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_applicants.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/developerdocs/api/api_authentication.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_authentication.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_authentication.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`authentication` -- Authentication Components Module
+****************************************************
+
+.. automodule:: waeup.kofa.authentication
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_browser.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_browser.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_browser.rst	(revision 10886)
@@ -0,0 +1,27 @@
+`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_interfaces.rst
+   browser/api_pages.rst
+   browser/api_viewlets.rst
+   browser/api_layout.rst
+   browser/api_breadcrumbs.rst
+   browser/api_theming.rst
+   browser/api_resources.rst
+   browser/api_exceptions.rst
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_catalog.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_catalog.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_catalog.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`catalog` -- Cataloging Components Module
+*****************************************
+
+.. automodule:: waeup.kofa.catalog
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_datacenter.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_datacenter.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_datacenter.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`datacenter` -- Datacenter Module
+*********************************
+
+.. automodule:: waeup.kofa.datacenter
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_hostels.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_hostels.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_hostels.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/developerdocs/api/api_imagestorage.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_imagestorage.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_imagestorage.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`imagestorage` -- Storing images and other files
+************************************************
+
+.. automodule:: waeup.kofa.imagestorage
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_interfaces.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_interfaces.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_interfaces.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/developerdocs/api/api_payments.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_payments.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_payments.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/developerdocs/api/api_permissions.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_permissions.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_permissions.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`permissions` -- Permissions and Roles Components Module
+********************************************************
+
+.. automodule:: waeup.kofa.permissions
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_schema.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_schema.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_schema.rst	(revision 10886)
@@ -0,0 +1,21 @@
+`schema` -- Schema Fields
+*************************
+
+(see also: :ref:`api-schema-modules`)
+
+.. automodule:: waeup.kofa.utils
+   :members:
+   :undoc-members:
+
+.. _api-schema-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   schema/api_field
+
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_students.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_students.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_students.rst	(revision 10886)
@@ -0,0 +1,36 @@
+`students` -- Student 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/tags/0.2/docs/source/developerdocs/api/api_university.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_university.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_university.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/developerdocs/api/api_userscontainer.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_userscontainer.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_userscontainer.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`userscontainer` -- Users (Principals) Module
+*********************************************
+
+.. automodule:: waeup.kofa.userscontainer
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_utils.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_utils.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_utils.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/developerdocs/api/api_widgets.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_widgets.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_widgets.rst	(revision 10886)
@@ -0,0 +1,23 @@
+`widgets` -- Widgets
+********************
+
+(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_restwidget
+   widgets/api_objectwidget
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_workflow.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_workflow.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/api_workflow.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`workflow` -- Workflow Components Module
+****************************************
+
+.. automodule:: waeup.kofa.workflow
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_applicant.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_applicant.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_applicant.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`applicants.applicant` - Applicant Components
+*********************************************
+
+.. automodule:: waeup.kofa.applicants.applicant
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_authentication.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_authentication.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_authentication.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`applicants.authentication` - Applicants Authentication Components
+******************************************************************
+
+.. automodule:: waeup.kofa.applicants.authentication
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_batching.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_batching.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_batching.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`applicants.batching` - Batch Importer for Applicants
+*****************************************************
+
+.. automodule:: waeup.kofa.applicants.batching
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_browser.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_browser.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_browser.rst	(revision 10886)
@@ -0,0 +1,7 @@
+`applicants.browser` - Applicants UI Components
+***********************************************
+
+.. automodule:: waeup.kofa.applicants.browser
+   :members:
+
+   .. :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_container.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_container.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_container.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`applicants.container` - Container for Applicants
+*************************************************
+
+.. automodule:: waeup.kofa.applicants.container
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_interfaces.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_interfaces.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_interfaces.rst	(revision 10886)
@@ -0,0 +1,33 @@
+`applicants.interfaces` - Applicants Related Interfaces
+*******************************************************
+
+.. automodule:: waeup.kofa.applicants.interfaces
+   :members:
+   :undoc-members:
+
+   .. autointerface:: IApplicantsRoot
+      :members:
+
+   .. autointerface:: IApplicantsContainer
+      :members:
+
+   .. autointerface:: IApplicantsContainerAdd
+      :members:
+
+   .. autointerface:: IApplicantBaseData
+      :members:
+
+   .. autointerface:: IApplicant
+      :members:
+
+   .. autointerface:: IApplicantEdit
+      :members:
+
+   .. autointerface:: IApplicantUpdateByRegNo
+      :members:
+
+   .. autointerface:: IApplicantOnlinePayment
+      :members:
+
+   .. autointerface:: IApplicantsContainerProvider
+      :members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_permissions.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_permissions.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_permissions.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`applicants.permissions` - Applicants Permission Components
+***********************************************************
+
+.. automodule:: waeup.kofa.applicants.permissions
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_root.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_root.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_root.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`applicants.root` - Root for Applicants Containers
+**************************************************
+
+.. automodule:: waeup.kofa.applicants.root
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_workflow.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_workflow.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/applicants/api_workflow.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`applicants.workflow` - Applicants Workflow Components
+******************************************************
+
+.. automodule:: waeup.kofa.applicants.workflow
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_breadcrumbs.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_breadcrumbs.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_breadcrumbs.rst	(revision 10886)
@@ -0,0 +1,7 @@
+`browser.breadcrumbs` - Browser Breadcrumbs
+*******************************************
+
+.. automodule:: waeup.kofa.browser.breadcrumbs
+   :members:
+   :undoc-members:
+
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_exceptions.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_exceptions.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_exceptions.rst	(revision 10886)
@@ -0,0 +1,7 @@
+`browser.exceptions` - Browser Exceptions
+*****************************************
+
+.. automodule:: waeup.kofa.browser.exceptions
+   :members:
+   :undoc-members:
+
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_interfaces.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_interfaces.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_interfaces.rst	(revision 10886)
@@ -0,0 +1,18 @@
+`browser.interfaces` - Browser Interfaces
+*****************************************
+
+.. automodule:: waeup.kofa.browser.interfaces
+   :members:
+   :undoc-members:
+
+   .. autointerface:: IBreadcrumb
+      :members:
+
+   .. autointerface:: IBreadcrumbIgnorable
+      :members:
+
+   .. autointerface:: IBreadcrumbContainer
+      :members:
+
+   .. autointerface:: ITheme
+      :members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_layout.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_layout.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_layout.rst	(revision 10886)
@@ -0,0 +1,7 @@
+`browser.layout` - Browser Layout
+*********************************
+
+.. automodule:: waeup.kofa.browser.layout
+   :members:
+   :undoc-members:
+
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_pages.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_pages.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_pages.rst	(revision 10886)
@@ -0,0 +1,7 @@
+`browser.pages` - Browser Pages (Views)
+***************************************
+
+.. automodule:: waeup.kofa.browser.pages
+   :members:
+   :undoc-members:
+
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_resources.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_resources.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_resources.rst	(revision 10886)
@@ -0,0 +1,7 @@
+`browser.resources` - Browser Resources
+***************************************
+
+.. automodule:: waeup.kofa.browser.resources
+   :members:
+   :undoc-members:
+
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_theming.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_theming.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_theming.rst	(revision 10886)
@@ -0,0 +1,7 @@
+`browser.theming` - Browser Theming
+***********************************
+
+.. automodule:: waeup.kofa.browser.theming
+   :members:
+   :undoc-members:
+
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_viewlets.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_viewlets.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/browser/api_viewlets.rst	(revision 10886)
@@ -0,0 +1,12 @@
+`browser.viewlets` - 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.browser.viewlets
+     :members:
+     :undoc-members:
+
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_browser.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_browser.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_browser.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/developerdocs/api/hostels/api_catalog.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_catalog.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_catalog.rst	(revision 10886)
@@ -0,0 +1,8 @@
+`hostels.catalog` - Hostel Catalog
+**********************************
+
+.. automodule:: waeup.kofa.hostels.catalog
+   :members:
+   :undoc-members:
+
+   .. class:: BedIndexes
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_container.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_container.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_container.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`hostels.container` - Hostels Container Components
+**************************************************
+
+.. automodule:: waeup.kofa.hostels.container
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_hostel.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_hostel.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_hostel.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`hostels.hostel` - Hostel Components
+************************************
+
+.. automodule:: waeup.kofa.hostels.hostel
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_interfaces.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_interfaces.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_interfaces.rst	(revision 10886)
@@ -0,0 +1,18 @@
+`hostels.interfaces` - Hostel Related Interfaces
+************************************************
+
+.. automodule:: waeup.kofa.hostels.interfaces
+   :members:
+   :undoc-members:
+
+   .. autointerface:: IHostelsContainer
+      :members:
+
+   .. autointerface:: IHostel
+      :members:
+
+   .. autointerface:: IBed
+      :members:
+
+   .. autointerface:: IBedAllocateStudent
+      :members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_permissions.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_permissions.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_permissions.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`hostels.permissions` - Hostel Permissions
+******************************************
+
+.. automodule:: waeup.kofa.hostels.permissions
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_vocabularies.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_vocabularies.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/hostels/api_vocabularies.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`hostels.vocabularies` - Hostel Vocabularies
+********************************************
+
+.. automodule:: waeup.kofa.hostels.vocabularies
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/index.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/index.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/index.rst	(revision 10886)
@@ -0,0 +1,33 @@
+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_authentication
+   api_browser
+   api_catalog
+   api_datacenter
+   api_hostels
+   api_imagestorage
+   api_interfaces
+   api_payments
+   api_permissions
+   api_schema
+   api_university
+   api_userscontainer
+   api_utils
+   api_students
+   api_widgets
+   api_workflow
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/payments/api_container.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/payments/api_container.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/payments/api_container.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`payments.container` - Payments Container Components
+****************************************************
+
+.. automodule:: waeup.kofa.payments.container
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/payments/api_interfaces.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/payments/api_interfaces.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/payments/api_interfaces.rst	(revision 10886)
@@ -0,0 +1,18 @@
+`payments.interfaces` - Payment Interfaces
+******************************************
+
+.. automodule:: waeup.kofa.payments.interfaces
+   :members:
+   :undoc-members:
+
+   .. autointerface:: IPaymentsContainer
+      :members:
+
+   .. autointerface:: IPayment
+      :members:
+
+   .. autointerface:: ISCPayment
+      :members:
+
+   .. autointerface:: IOnlinePayment
+      :members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/payments/api_payment.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/payments/api_payment.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/payments/api_payment.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`payments.payment` - Payment Components
+***************************************
+
+.. automodule:: waeup.kofa.payments.payment
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/schema/api_field.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/schema/api_field.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/schema/api_field.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`schema.field` - Special Fields
+*******************************
+
+.. automodule:: waeup.kofa.schema.field
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_accommodation.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_accommodation.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_accommodation.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`students.accommodation` - Student Accommodation Components
+***********************************************************
+
+.. automodule:: waeup.kofa.students.accommodation
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_authentication.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_authentication.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_authentication.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`students.authentication` - Student Authentication Components
+*************************************************************
+
+.. automodule:: waeup.kofa.students.authentication
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_batching.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_batching.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_batching.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`students.batching` - Student Import and Export Components
+**********************************************************
+
+.. automodule:: waeup.kofa.students.batching
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_browser.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_browser.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_browser.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`students.browser` - Students UI Components
+*******************************************
+
+.. automodule:: waeup.kofa.students.browser
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_catalog.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_catalog.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_catalog.rst	(revision 10886)
@@ -0,0 +1,8 @@
+`students.catalog` - Students Catalog Components
+************************************************
+
+.. automodule:: waeup.kofa.students.catalog
+   :members:
+   :undoc-members:
+
+   .. class:: StudentIndexes
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_container.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_container.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_container.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`students.container` - Students Container
+*****************************************
+
+.. automodule:: waeup.kofa.students.container
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_dynamicroles.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_dynamicroles.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_dynamicroles.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`students.dynamicroles` - Dynamic Roles Components
+**************************************************
+
+.. automodule:: waeup.kofa.students.dynamicroles
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_interfaces.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_interfaces.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_interfaces.rst	(revision 10886)
@@ -0,0 +1,60 @@
+`students.interfaces` - Students Related Interfaces
+***************************************************
+
+.. automodule:: waeup.kofa.students.interfaces
+   :members:
+   :undoc-members:
+
+   .. autointerface:: IStudentsUtils
+      :members:
+
+   .. autointerface:: IStudentsContainer
+      :members:
+
+   .. autointerface:: IStudentNavigation
+      :members:
+
+   .. autointerface:: IStudentBase
+      :members:
+
+   .. autointerface:: IStudentClearance
+      :members:
+
+   .. autointerface:: IStudentPersonal
+      :members:
+
+   .. autointerface:: IStudent
+      :members:
+
+   .. autointerface:: IStudentUpdateByRegNo
+      :members:
+
+   .. autointerface:: IStudentUpdateByMatricNo
+      :members:
+
+   .. autointerface:: IStudentStudyCourse
+      :members:
+
+   .. autointerface:: IStudentStudyLevel
+      :members:
+
+   .. autointerface:: ICourseTicket
+      :members:
+
+   .. autointerface:: ICourseTicketAdd
+      :members:
+
+   .. autointerface:: IStudentAccommodation
+      :members:
+
+   .. autointerface:: IBedTicket
+      :members:
+
+   .. autointerface:: IStudentPaymentsContainer
+      :members:
+
+   .. autointerface:: IStudentOnlinePayment
+      :members:
+
+   .. autointerface:: IStudentChangePassword
+      :members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_payments.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_payments.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_payments.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`students.payments` - Student Payments
+**************************************
+
+.. automodule:: waeup.kofa.students.payments
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_permissions.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_permissions.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_permissions.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`students.permissions` - Student Permissions
+********************************************
+
+.. automodule:: waeup.kofa.students.permissions
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_student.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_student.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_student.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`students.student` - Student Components
+***************************************
+
+.. automodule:: waeup.kofa.students.student
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_studycourse.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_studycourse.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_studycourse.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`students.studycourse` - Student Study Course Components
+********************************************************
+
+.. automodule:: waeup.kofa.students.studycourse
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_studylevel.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_studylevel.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_studylevel.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`students.studylevel` - Student Study Level Components
+******************************************************
+
+.. automodule:: waeup.kofa.students.studylevel
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_utils.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_utils.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_utils.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`students.utils` - Student Utilities
+************************************
+
+.. automodule:: waeup.kofa.students.utils
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_viewlets.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_viewlets.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_viewlets.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/developerdocs/api/students/api_vocabularies.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_vocabularies.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_vocabularies.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`students.vocabularies` - Student Vocabularies
+**********************************************
+
+.. automodule:: waeup.kofa.students.vocabularies
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_workflow.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_workflow.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/students/api_workflow.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`students.workflow` - Student Workflow
+**************************************
+
+.. automodule:: waeup.kofa.students.workflow
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_batching.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_batching.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_batching.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`university.batching` - Importer Components
+*******************************************
+
+.. automodule:: waeup.kofa.university.batching
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_catalog.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_catalog.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_catalog.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`university.catalog` - Cataloging Components
+********************************************
+
+.. automodule:: waeup.kofa.university.catalog
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_certificate.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_certificate.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_certificate.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`university.certificate` - Certificate Components
+*************************************************
+
+.. automodule:: waeup.kofa.university.certificate
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_certificatescontainer.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_certificatescontainer.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_certificatescontainer.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`university.certificatescontainer` - Certificates Container Components
+**********************************************************************
+
+.. automodule:: waeup.kofa.university.certificatescontainer
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_course.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_course.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_course.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`university.course` - Course Components
+***************************************
+
+.. automodule:: waeup.kofa.university.course
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_coursescontainer.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_coursescontainer.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_coursescontainer.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`university.coursescontainer` - Courses Container Components
+************************************************************
+
+.. automodule:: waeup.kofa.university.coursescontainer
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_department.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_department.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_department.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`university.department` - Department Components
+***********************************************
+
+.. automodule:: waeup.kofa.university.department
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_facultiescontainer.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_facultiescontainer.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_facultiescontainer.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`university.facultiescontainer` - Faculties Container Components
+****************************************************************
+
+.. automodule:: waeup.kofa.university.facultiescontainer
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_faculty.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_faculty.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_faculty.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`university.faculty` - Faculty Components
+*****************************************
+
+.. automodule:: waeup.kofa.university.faculty
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_interfaces.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_interfaces.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_interfaces.rst	(revision 10886)
@@ -0,0 +1,45 @@
+`university.interfaces` - Interfaces of the University Package
+**************************************************************
+
+.. automodule:: waeup.kofa.university.interfaces
+   :members:
+   :undoc-members:
+
+.. autointerface:: waeup.kofa.university.interfaces.IFaculty
+   :members:
+
+.. autointerface:: waeup.kofa.university.interfaces.IFacultyAdd
+   :members:
+
+.. autointerface:: waeup.kofa.university.interfaces.IFacultiesContainer
+   :members:
+
+.. autointerface:: waeup.kofa.university.interfaces.IDepartment
+   :members:
+
+.. autointerface:: waeup.kofa.university.interfaces.IDepartmentAdd
+   :members:
+
+.. autointerface:: waeup.kofa.university.interfaces.ICoursesContainer
+   :members:
+
+.. autointerface:: waeup.kofa.university.interfaces.ICourse
+   :members:
+
+.. autointerface:: waeup.kofa.university.interfaces.ICourseAdd
+   :members:
+
+.. autointerface:: waeup.kofa.university.interfaces.ICertificate
+   :members:
+
+.. autointerface:: waeup.kofa.university.interfaces.ICertificateAdd
+   :members:
+
+.. autointerface:: waeup.kofa.university.interfaces.ICertificatesContainer
+   :members:
+
+.. autointerface:: waeup.kofa.university.interfaces.ICertificateCourse
+   :members:
+
+.. autointerface:: waeup.kofa.university.interfaces.ICertificateCourseAdd
+   :members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_vocabularies.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_vocabularies.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/university/api_vocabularies.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`university.vocabularies` - Vocabularies and Sources
+****************************************************
+
+.. automodule:: waeup.kofa.university.vocabularies
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_batching.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_batching.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_batching.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`utils.batching` - Batching Components
+**************************************
+
+.. automodule:: waeup.kofa.utils.batching
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_converters.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_converters.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_converters.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`utils.converters` - Converter Components
+*****************************************
+
+.. automodule:: waeup.kofa.utils.converters
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_helpers.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_helpers.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_helpers.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`utils.helpers` - Helper Components
+***********************************
+
+.. automodule:: waeup.kofa.utils.helpers
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_logger.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_logger.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_logger.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`utils.logger` - Logger Components
+**********************************
+
+.. automodule:: waeup.kofa.utils.logger
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_utils.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_utils.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/utils/api_utils.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`utils.utils` - Utilities
+*************************
+
+.. automodule:: waeup.kofa.utils.utils
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/widgets/api_datewidget.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/widgets/api_datewidget.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/widgets/api_datewidget.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`widgets.datewidget` - Date Widget
+**********************************
+
+.. automodule:: waeup.kofa.widgets.datewidget
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/widgets/api_objectwidget.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/widgets/api_objectwidget.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/widgets/api_objectwidget.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`widgets.objectwidget` - Object Widget
+**************************************
+
+.. automodule:: waeup.kofa.widgets.objectwidget
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/widgets/api_phonewidget.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/widgets/api_phonewidget.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/widgets/api_phonewidget.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`widgets.phonewidget` - Phone Widget
+************************************
+
+.. automodule:: waeup.kofa.widgets.phonewidget
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/widgets/api_restwidget.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/widgets/api_restwidget.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/api/widgets/api_restwidget.rst	(revision 10886)
@@ -0,0 +1,6 @@
+`widgets.restwidget` - ReST Widget
+**********************************
+
+.. automodule:: waeup.kofa.widgets.restwidget
+   :members:
+   :undoc-members:
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/developernotes.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/developernotes.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/developernotes.rst	(revision 10886)
@@ -0,0 +1,344 @@
+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
+===========================
+
+The installation is described for Linux-based computers.
+
+Preparing the system
+--------------------
+
+To create a working copy of the WAeUP.Kofa we recommend use of
+`virtualenv`. You, however, need also some basic libraries, a C
+compiler and some things more.
+
+What you need (Debian/Ubuntu package names in brackets):
+
+* Python 2.6 (python2.6)
+
+  Currently, also Python2.5 is supported but we want to make use of
+  some of the 2.6 goodies in the future.
+
+
+* Python 2.6 development files (python2.6-dev)
+
+* A C-Compiler (gcc)
+
+* The C library development files (libc6-dev)
+
+* A subversion client (svn)
+
+* enscript (enscript) [optional]
+
+  This is only needed if you want test coverage reports.
+
+All these packages can be installed on Debian systems like this::
+
+  # apt-get install python2.6 python2.6-dev python2.6-dbg \
+                    gcc libc6-dev svn enscript
+
+Afterwards you should be able to enter::
+
+  $ python2.6
+
+at the commandline and get a Python prompt. Quit the interpreter
+pressing <CTRL-D>.
+
+Installing `virtualenv`
+-----------------------
+
+We recommend use of `virtualenv` to create Python sandboxes where you
+can run your code without touching any other installations.
+
+If you don't already have ``easy_install`` available, you can find the
+script to set it up on the `PEAK EasyInstall page`_.
+
+.. _`PEAK EasyInstall page`: http://peak.telecommunity.com/DevCenter/EasyInstall#installing-easy-install
+
+You need to download `ez_setup.py`_. Then, you run it like this to
+install ``easy_install`` into your system Python::
+
+  $ sudo python2.6 ez_setup.py
+
+.. _`ez_setup.py`: http://peak.telecommunity.com/dist/ez_setup.py
+
+This will make ``easy_install`` available to you.
+
+.. note:: Sometimes you have ``easy_install`` installed but you need a
+          newer version of the underlying setuptools infrastructure to
+          make Grok work. You can upgrade setuptools with::
+
+            $ sudo easy_install -U setuptools
+
+Now you can install `virtualenv` by doing (as root)::
+
+  # easy_install-2.6 virtualenv
+
+This step will fetch all needed sources from the internet and install
+`virtualenv` locally in your Python2.6 installation.
+
+
+Creating a sandbox
+------------------
+
+This step is only necessary (and recommended) if you installed
+`virtualenv` before.
+
+As a normal user you now can create a sandbox for your upcoming work
+by::
+
+  $ virtualenv --no-site-packages mysandbox
+
+where ``mysandbox`` is a directory in the filesystem where your
+sandbox will be created. `virtualenv` will also create this directory
+for you.
+
+By passing the ``no-site-packages`` switch we tell `virtualenv` to
+provide us a clean environment without any extra-packages installed
+systemwide.
+
+If you have a look into the freshly created sandbox, you will notice
+that in the ``bin/`` directory there is also 
+
+You now can activate the sandbox by doing::
+
+  $ source mysandbox/bin/activate
+
+You will notice that the input prompt changes.
+
+To deactivate the sandbox at any time, enter::
+
+  $ deactivate
+
+and the prompt will be the same as before the activation.
+
+For the following steps make sure the sandbox is active.
+
+
+Creating a working place
+------------------------
+
+In the sandbox (or anywhere else) we now create our real working
+environment. To do this, we change to the sandbox and checkout the
+sources of the WAeUP.Kofa from the subversion server::
+
+  $ cd mysandbox/
+  $ svn co https://svn.waeup.org/repos/main/waeup.kofa/trunk waeup-trunk
+
+where ``waeup-trunk`` is only a name we've chosen here to make clear
+where the sources come from.
+
+This command should fetch the sources of the WAeUP sources for you and
+put it in the directory ``waeup-trunk/``.
+
+Now enter the new directory::
+
+  $ cd waeup-trunk
+
+
+Preparing the build
+-------------------
+
+In the sources directory (``waeup-trunk/``) you have to prepare the
+project to fetch needed components (eggs), compile C-code parts,
+etc. This steip will not touch any external projects::
+
+  $ python2.6 bootstrap.py
+
+This will generate some directories and the ``buildout`` script in
+``bin/`` for us. This step must be executed only once for each
+instance.
+
+Now we can do the real build by triggering::
+
+  $ 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`` or ``setup.py``.
+
+Afterwards we are ready to go.
+
+
+Start the instance
+------------------
+
+You should be able now to start the created instance by doing::
+
+   $ bin/zopectl fg
+
+If you now point a browser to::
+
+   localhost:8080
+
+you should get a login pop-up, where you can login as superuser with
+``grok`` and ``grok`` as username/password.
+
+If you want to change the default credentials, have a look into
+``buildout.cfg`` where the superuser password is determined.
+
+You can stop the instance by pressing <CTRL-C>.
+
+
+
+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.
+
+Generating documentation
+------------------------
+
+We use the excellent `Sphinx <http://sphinx.pocoo.org/>`_ Python
+documentation generator to generate the docs as HTML pages.
+
+The documentation of the :mod:`waeup.kofa` project can easily be
+created doing::
+
+  $ bin/waeupdocs
+
+This will create a tree of HTML pages in
+``parts/waeupdocs/waeup.kofa/build/waeup.kofa/`` 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.
+
+
+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/tags/0.2/docs/source/developerdocs/index.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/index.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/index.rst	(revision 10886)
@@ -0,0 +1,9 @@
+Developer Documentation
+***********************
+
+.. toctree::
+   :maxdepth: 2
+   
+   developernotes
+   api/index
+   profiling
Index: main/waeup.kofa/tags/0.2/docs/source/developerdocs/profiling.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/developerdocs/profiling.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/developerdocs/profiling.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/documentation.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/documentation.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/documentation.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/glossary.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/glossary.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/glossary.rst	(revision 10886)
@@ -0,0 +1,16 @@
+.. _glossary:
+
+Glossary
+********
+
+.. glossary::
+
+   Kofa
+     Student Information and Registration Portal.
+
+   WAeUP
+     West African e-University Project.
+
+   waeup.kofa
+     A Python package explained in this documentation.
+
Index: main/waeup.kofa/tags/0.2/docs/source/index.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/index.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/index.rst	(revision 10886)
@@ -0,0 +1,11 @@
+.. WAeUP Student Information and Registration Portal (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/tags/0.2/docs/source/intro.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/intro.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/intro.rst	(revision 10886)
@@ -0,0 +1,1 @@
+.. include:: ../../README.txt
Index: main/waeup.kofa/tags/0.2/docs/source/license.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/license.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/license.rst	(revision 10886)
@@ -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/tags/0.2/docs/source/userdocs/buildingdocs.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/userdocs/buildingdocs.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/userdocs/buildingdocs.rst	(revision 10886)
@@ -0,0 +1,61 @@
+.. _building_documentation:
+
+Building Documentation (Linux OS only)
+**************************************
+
+To build the documentation for :mod:`waeup.kofa`, please follow these steps:
+
+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
+==================
+
+After successful install go to the root dir, where `waeup.kofa` is
+located.
+
+There run::
+
+   $ ./bin/waeupdocs
+
+This will (re-)generate any needed docs.
+
+The output can be found in ``docs/build/html``.
+
+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/tags/0.2/docs/source/userdocs/index.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/userdocs/index.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/userdocs/index.rst	(revision 10886)
@@ -0,0 +1,9 @@
+User Documentation
+******************
+
+.. toctree::
+   :maxdepth: 2
+
+   install
+   install_on_windows
+   buildingdocs
Index: main/waeup.kofa/tags/0.2/docs/source/userdocs/install.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/userdocs/install.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/userdocs/install.rst	(revision 10886)
@@ -0,0 +1,3 @@
+.. _installing_linux:
+
+.. include:: ../../INSTALL.txt
Index: main/waeup.kofa/tags/0.2/docs/source/userdocs/install_on_windows.rst
===================================================================
--- main/waeup.kofa/tags/0.2/docs/source/userdocs/install_on_windows.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/docs/source/userdocs/install_on_windows.rst	(revision 10886)
@@ -0,0 +1,3 @@
+.. _installing_win:
+
+.. include:: ../../INSTALLWIN.txt
Index: main/waeup.kofa/tags/0.2/etc/README.txt
===================================================================
--- main/waeup.kofa/tags/0.2/etc/README.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/etc/README.txt	(revision 10886)
@@ -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/tags/0.2/etc/debug.ini.in
===================================================================
--- main/waeup.kofa/tags/0.2/etc/debug.ini.in	(revision 10886)
+++ main/waeup.kofa/tags/0.2/etc/debug.ini.in	(revision 10886)
@@ -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/tags/0.2/etc/deploy.ini.in
===================================================================
--- main/waeup.kofa/tags/0.2/etc/deploy.ini.in	(revision 10886)
+++ main/waeup.kofa/tags/0.2/etc/deploy.ini.in	(revision 10886)
@@ -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/tags/0.2/etc/profile.ini.in
===================================================================
--- main/waeup.kofa/tags/0.2/etc/profile.ini.in	(revision 10886)
+++ main/waeup.kofa/tags/0.2/etc/profile.ini.in	(revision 10886)
@@ -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/tags/0.2/etc/site.zcml.in
===================================================================
--- main/waeup.kofa/tags/0.2/etc/site.zcml.in	(revision 10886)
+++ main/waeup.kofa/tags/0.2/etc/site.zcml.in	(revision 10886)
@@ -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/tags/0.2/etc/zdaemon.conf.in
===================================================================
--- main/waeup.kofa/tags/0.2/etc/zdaemon.conf.in	(revision 10886)
+++ main/waeup.kofa/tags/0.2/etc/zdaemon.conf.in	(revision 10886)
@@ -0,0 +1,8 @@
+<runner>
+  program ${buildout:directory}/bin/paster serve ${buildout:directory}/parts/etc/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/tags/0.2/etc/zeo1.conf.in
===================================================================
--- main/waeup.kofa/tags/0.2/etc/zeo1.conf.in	(revision 10886)
+++ main/waeup.kofa/tags/0.2/etc/zeo1.conf.in	(revision 10886)
@@ -0,0 +1,8 @@
+<runner>
+  program ${buildout:directory}/bin/paster serve --pid-file ${buildout:directory}/var/zeo1.pid ${buildout:directory}/parts/etc/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/tags/0.2/etc/zeo1.ini.in
===================================================================
--- main/waeup.kofa/tags/0.2/etc/zeo1.ini.in	(revision 10886)
+++ main/waeup.kofa/tags/0.2/etc/zeo1.ini.in	(revision 10886)
@@ -0,0 +1,96 @@
+# 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:main]
+use = egg:${kofa_params:devel_pkg}
+filter-with = translogger
+
+[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
Index: main/waeup.kofa/tags/0.2/etc/zeo2.conf.in
===================================================================
--- main/waeup.kofa/tags/0.2/etc/zeo2.conf.in	(revision 10886)
+++ main/waeup.kofa/tags/0.2/etc/zeo2.conf.in	(revision 10886)
@@ -0,0 +1,8 @@
+<runner>
+  program ${buildout:directory}/bin/paster serve --pid-file ${buildout:directory}/var/zeo2.pid ${buildout:directory}/parts/etc/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/tags/0.2/etc/zeo2.ini.in
===================================================================
--- main/waeup.kofa/tags/0.2/etc/zeo2.ini.in	(revision 10886)
+++ main/waeup.kofa/tags/0.2/etc/zeo2.ini.in	(revision 10886)
@@ -0,0 +1,96 @@
+# 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:main]
+use = egg:${kofa_params:devel_pkg}
+filter-with = translogger
+
+[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
Index: main/waeup.kofa/tags/0.2/etc/zope.conf.in
===================================================================
--- main/waeup.kofa/tags/0.2/etc/zope.conf.in	(revision 10886)
+++ main/waeup.kofa/tags/0.2/etc/zope.conf.in	(revision 10886)
@@ -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/tags/0.2/etc/zope_zeo1.conf.in
===================================================================
--- main/waeup.kofa/tags/0.2/etc/zope_zeo1.conf.in	(revision 10886)
+++ main/waeup.kofa/tags/0.2/etc/zope_zeo1.conf.in	(revision 10886)
@@ -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/tags/0.2/etc/zope_zeo2.conf.in
===================================================================
--- main/waeup.kofa/tags/0.2/etc/zope_zeo2.conf.in	(revision 10886)
+++ main/waeup.kofa/tags/0.2/etc/zope_zeo2.conf.in	(revision 10886)
@@ -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/tags/0.2/setup.py
===================================================================
--- main/waeup.kofa/tags/0.2/setup.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/setup.py	(revision 10886)
@@ -0,0 +1,131 @@
+import os
+from setuptools import setup, find_packages
+
+version = '0.2dev'
+
+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.yui',
+    'hurry.zoperesource',
+    'zc.sourcefactory',
+    'megrok.layout',
+    'reportlab',
+    'Pillow',
+    '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]',
+    ],
+
+# 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('src', 'waeup', 'kofa', 'app.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',
+        'Framework :: Zope3',
+        'Topic :: Education',
+        'Topic :: Internet :: WWW/HTTP',
+        ],
+      author = "The WAeUP team.",
+      author_email = "",
+      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,
+        ),
+      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/tags/0.2/src/waeup/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/__init__.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/__init__.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/accesscodes/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/__init__.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/accesscodes/accesscode.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/accesscode.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/accesscode.py	(revision 10886)
@@ -0,0 +1,664 @@
+## $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, ))
+        if not 'TSC' in site['accesscodes']:
+            site['accesscodes'].createBatch(datetime.utcnow(), 'system',
+                'TSC', 0.0, 0)
+            logger.info('Empty TSC access code batch added.')
+        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 owner 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/tags/0.2/src/waeup/kofa/accesscodes/accesscode.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/accesscode.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/accesscode.txt	(revision 10886)
@@ -0,0 +1,674 @@
+:mod:`waeup.kofa.accesscodes.accesscode` -- access codes (aka PINs)
+*******************************************************************
+
+.. module:: waeup.kofa.accesscodes.accesscode
+
+Components that represent access codes and related.
+
+.. :NOdoctest:
+.. :NOlayer: waeup.kofa.testing.KofaUnitTestLayer
+
+About access-codes
+==================
+
+Access codes are ids used to grant first-time access to the system for
+students.
+
+They are normally not generated by third-party components but always
+part of batches.
+
+An access-code consists of three parts::
+
+    APP-12-0123456789
+    ^^^ ^^ ^^^^^^^^^^
+     A  B  C
+
+where ``A`` tells about the purpose of the code, ``B`` gives the
+number of batch the code belongs to (1 to 3 digits), and ``C`` is a
+unique random number of 10 digits.
+
+For the generation of the random number :mod:`waeup.kofa` requires a
+'urandom' entropy provider which is available with most standard
+Unix/Linux systems. This makes the generated numbers relatively
+secure, especially when compared with recent PHP-based applications.
+
+
+AccessCode
+==========
+
+.. class:: AccessCode(batch_serial, random_num[,invalidation_date=None[, student_id=None]])
+
+   You normally shouldn't create standalone access-codes. Use
+   instances of :class:`AccessCodeBatch` instead as they generate them
+   (in masses) and care for them.
+
+   Note, that :class:`AccessCode` instances are not persistent on
+   themselves. They have to be stored inside a persistent object (like
+   :class:`AccessCodeBatch`) to be kept.
+
+   Access-codes can have three states: unused, used, and
+   disabled. While still unused but enabled access-codes are reflected
+   by an empty ``invalidation_date`` (set to ``None``), an already
+   used (invalidated) code provides an invalidation date.
+
+   In case of misuse or similar cases access-codes can also be
+   completely disabled by setting the ``disabled`` attribute to
+   ``True``.
+
+   The class implements
+   :mod:`waeup.kofa.accesscodes.interfaces.IAccessCode`:
+
+    >>> from waeup.kofa.accesscodes.interfaces import IAccessCode
+    >>> from waeup.kofa.accesscodes.accesscodes import AccessCode
+    >>> from zope.interface.verify import verifyClass
+    >>> verifyClass(IAccessCode, AccessCode)
+    True
+
+   .. attribute:: representation
+
+      The 'full' id of an access-code as described above. Something
+      like ``'APP-12-0123456789'``.
+
+      Read-only attribute.
+
+   .. attribute:: batch_serial
+
+      Serial number of this access-code inside the batch.
+
+      .. note:: XXX: Do we really need this?
+
+         Subject to be dropped.
+
+   .. attribute:: student_id
+
+      A string or ``None``. Set when an access-code is
+      invalidated. ``None`` by default.
+
+   .. attribute:: batch_prefix
+
+      The prefix of the batch the access-code belongs to.
+
+      Read-only attribute.
+
+   .. attribute:: batch_num
+
+      The number of the batch the access-code belongs to.
+
+      Read-only attribute.
+
+   .. attribute:: cost
+
+      What the access-code costs. A float.
+
+      Read-only attribute.
+
+   .. attribute:: invalidation_date
+
+      Python datetime when the access code was invalidated, or
+      ``None``. ``None`` by default.
+
+      If an access-code is disabled, this attribute contains the
+      datetime of disabling.
+
+      Read-only attribute. Only batches are supposed to set this value.
+
+   .. attribute:: disabled
+
+      Boolean, ``False`` by default. When set to ``True``, this
+      access-code should not be used any more.
+
+      Read-only attribute. Only batches are supposed to set this value.
+
+   Access codes that are not part of a batch, will give strange
+   representations:
+
+    >>> ac = AccessCode(None, '9999999999')
+    >>> ac.representation
+    '--<10-DIGITS>'
+
+   Also the ``cost`` will not be set:
+
+    >>> ac.cost is None
+    True
+
+
+AccessCodeBatch
+===============
+
+.. class:: AccessCodeBatch(creation_date, creator, batch_prefix, cost, entry_num, num)
+
+   Create a batch of access-codes.
+
+   :param creation_date: python datetime
+   :param creator: creators user id
+   :type creator: string
+   :param batch_prefix: prefix of this batch
+   :param cost: cost per access code
+   :type cost: float
+   :param entry_num: number of access codes to create
+   :param num: number of this batch
+
+   A persistent :class:`grok.Model`. It implements
+   :class:`waeup.kofa.accesscodes.interfaces.IAccessCodeBatch`.
+
+   When creating a batch, all entries (access-codes) are generated as
+   well.
+
+   .. attribute:: creation_date
+
+      The datetime when the batch was created.
+
+   .. attribute:: creator
+
+      String with user id of the user that generated the batch.
+
+   .. attribute:: cost
+
+      Float representing the costs for a single access-code. All
+      entries inside the batch share the same cost.
+
+   .. attribute:: entry_num
+
+      Number of entries (access-codes) inside the batch.
+
+   .. attribute:: invalidated_num
+
+      Number of entries that were already invalidated.
+
+   .. attribute:: prefix
+
+      Prefix of the batch. This tells about the purpose of this batch.
+
+   .. attribute:: num
+
+      Number of this batch. For a certain prefix there can exist
+      several batches, which are numbered in increasing order. The
+      number is normally computed by the
+      :class:`AccessCodeBatchContainer` in which batches are
+      stored.
+
+      .. seealso:: :class:`AccessCodeBatchContainer`
+
+   .. method:: entries()
+
+      Get all accesscodes stored in the batch.
+
+      Returns a generator over all stored entries.
+
+   .. method:: getAccessCode(acesscode_id)
+
+      Get the :class:`AccessCode` object for the given
+      ``accesscode_id``.
+
+      Certain single access codes can be accessed inside a batch by
+      their representation (i.e. something like ``'APP-12-0123456789'``.
+
+      When a code cannot be found :exc:`KeyError` is raised.
+
+   .. method:: getAccessCodeForStudentId(student_id)
+
+      Get the :class:`AccessCode` object for the given
+      ``student_id``.
+
+      Certain single access codes can be accessed inside a batch by
+      their student_id. The student_id must be some string; ``None``
+      is not a valid value.
+
+      When a code cannot be found :exc:`KeyError` is raised.
+
+   .. method:: addAccessCode(serial_num, random_num)
+
+      Add an access code to the batch.
+
+      ``serial_num`` denotes the serial number of the new access-code
+      inside the batch. ``random_num`` is a string of 10 digits unique
+      in this batch.
+
+   .. method:: invalidate(ac_id[, student_id=None])
+
+      Invalidate the access-code with ID ``ac_id``.
+
+      Sets also the ``student_id`` attribute of the respective
+      :class:`AccessCode` entry.
+
+   .. method:: disable(ac_id, user_id)
+
+      Disable the access-code with ID ``ac_id``.
+
+     ``user_id`` is the user ID of the user triggering the
+      process. Already disabled ACs are left untouched.
+
+      Sets also the ``student_id`` and ``invalidation_date``
+      attributes of the respective :class:`AccessCode` entry. While
+      ``student_id`` is set to the given ``user_id``,
+      ``invalidation_date`` is set to current datetime.
+
+      Disabled access codes are supposed not to be used any more at
+      all.
+
+   .. method:: enable(ac_id)
+
+      (Re-)enable the access-code with ID ``ac_id``.
+
+      This leaves the given AC in state ``unused``. Already enabled
+      ACs are left untouched.
+
+      Sets ``student_id`` and ``invalidation_date`` values of the
+      respective :class:`AccessCode` entry to ``None``.
+
+   .. method:: createCSVLogFile()
+
+      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.
+
+   .. method:: archive()
+
+      Create a CSV file for archive. Archive files contain also
+      ``student_id`` and ``invalidation_date``.
+
+      Returns name of created file.
+
+   .. method:: search(searchterm, searchtype)
+
+      Search the batch for entries that comply with ``searchterm`` and
+      ``searchtype``.
+
+      ``searchtype`` must be one of ``serial``, ``pin``, or
+      ``stud_id`` and specifies, what kind of data is contained in the
+      ``searchterm``.
+
+      - ``serial`` looks for the AC with the ``serial_batch`` set to
+        the given (integer) number in ``searchterm``. Here
+        ``searchterm`` must be an int number.
+
+      - ``pin`` looks for the AC with the ``representation`` set to
+        the given string in ``searchterm``. Here ``searchterm`` must
+        be a string containing the full AC-ID like
+        ``APP-1-0123456789``.
+
+      - ``stud_id`` looks for the AC with the ``student_id`` set to
+        the given string in ``searchterm``. Here ``searchterm`` must
+        be a string containing the full student ID.
+
+      Lookup is done via local BTrees and therefore pretty fast.
+
+      Returns a list of access-codes found or empty list.
+
+
+Examples
+--------
+
+:class:`AccessCodeBatch` implements :class:`IAccessCodeBatch`:
+
+    >>> from waeup.kofa.accesscodes.interfaces import IAccessCodeBatch
+    >>> from waeup.kofa.accesscodes.accesscodes import AccessCodeBatch
+    >>> from zope.interface.verify import verifyClass
+    >>> verifyClass(IAccessCodeBatch, AccessCodeBatch)
+    True
+
+Creating a batch of three access-codes, with a cost of ``12.12`` per
+code, the batch prefix ``APP``, batch number ``10``, creator ID
+``Fred`` and some arbitrary creation date:
+
+    >>> import datetime
+    >>> from waeup.kofa.accesscodes.accesscodes import AccessCodeBatch
+    >>> batch = AccessCodeBatch(
+    ...   datetime.datetime(2009, 12, 23), 'Fred','APP', 12.12, 3, num=10)
+
+Getting all access-codes from a batch:
+
+    >>> ac_codes = batch.entries()
+    >>> ac_codes
+    <generator object...at 0x...>
+
+    >>> [x.representation for x in ac_codes]
+    ['APP-10-<10-DIGITS>', 'APP-10-<10-DIGITS>', 'APP-10-<10-DIGITS>']
+
+    >>> [x for x in batch.entries()]
+    [<waeup.kofa...AccessCode object at 0x...>, ...]
+
+Getting a single entry from the batch:
+
+    >>> ac_id = list(batch.entries())[0].representation
+    >>> ac = batch.getAccessCode(ac_id)
+    >>> ac
+    <waeup.kofa...AccessCode object at 0x...>
+
+    >>> ac is list(batch.entries())[0]
+    True
+
+Trying to get a single not-existent entry from a batch:
+
+    >>> batch.getAccessCode('blah')
+    Traceback (most recent call last):
+    ...
+    KeyError: 'blah'
+
+Invalidating an entry:
+
+    >>> batch.invalidated_num
+    0
+
+#
+#    >>> str(ac.invalidation_date), str(ac.student_id)
+#    ('None', 'None')
+#
+
+    >>> batch.invalidate(ac_id)
+    >>> batch.invalidated_num
+    1
+
+#    >>> ac.invalidation_date , str(ac.student_id)
+#    (datetime.datetime(...), 'None')
+#
+#    >>> batch.invalidate(ac_id, 'some_user_id')
+#    >>> ac.student_id
+#    'some_user_id'
+#
+#Getting a single entry by student_id:
+#
+#    >>> batch.getAccessCodeForStudentId('some_user_id')
+#    <waeup.kofa...AccessCode object at 0x...>
+#
+#Non-existent values will cause a :exc:`KeyError`:
+#
+#    >>> batch.getAccessCodeForStudentId('non-existing')
+#    Traceback (most recent call last):
+#    ...
+#    KeyError: 'non-existing'
+#
+#Already enabled entries will be left untouched when trying to renable
+#them:
+
+#    >>> batch.enable(ac_id)
+#    >>> ac.student_id
+#    'some_user_id'
+
+Disabling an entry:
+
+#    >>> batch.disabled_num
+#    0
+#
+#    >>> ac.disabled
+#    False
+#
+#    >>> batch.disable(ac_id, 'some userid')
+#    >>> ac.disabled
+#    True
+#
+#    >>> ac.student_id
+#    'some userid'
+#
+#    >>> batch.disabled_num
+#    1
+
+Already disabled entries will not be disabled again:
+
+#    >>> batch.disable(ac_id, 'other userid')
+#    >>> ac.student_id
+#    'some userid'
+
+Reenabling an entry:
+
+#    >>> batch.enable(ac_id)
+#    >>> ac.disabled
+#    False
+#
+#    >>> ac.student_id is None
+#    True
+#
+#    >>> ac.invalidation_date is None
+#    True
+
+Access codes get their ``cost`` from the batch they belong to. Note,
+that it is advisable to print costs always using a format, as Python
+floats are often represented by irritating values:
+
+    >>> ac.cost
+    12.119999999999999
+
+    >>> print "%0.2f" % ac.cost
+    12.12
+
+Searching for serials:
+
+    >>> result = batch.search(0, 'serial')
+    >>> result
+    [<waeup.kofa...AccessCode object at 0x...>]
+
+    >>> result[0].batch_serial
+    0
+
+Searching for AC-IDs:
+
+    >>> result = batch.search(ac.representation, 'pin')
+    >>> result[0].representation
+    'APP-...-...'
+
+Searching for student IDs:
+
+#    >>> batch.invalidate(ac_id, 'some_new_user_id')
+#    >>> result = batch.search('some_new_user_id', 'stud_id')
+#    >>> result[0].student_id
+#    'some_new_user_id'
+
+Searching for not existing entries will return empty lists:
+
+    >>> batch.search(12, 'serial')
+    []
+
+    >>> batch.search('not-a-valid-pin', 'pin')
+    []
+
+#    >>> batch.search('not-a-student-id', 'stud_id')
+#    []
+
+#    >>> batch.search('blah', 'not-a-valid-searchtype')
+#    []
+
+AccessCodeBatchContainer
+========================
+
+.. class:: AccessCodeBatchContainer()
+
+   A container for access code batches.
+
+   .. method:: addBatch(batch)
+
+      Add a batch in this container. You should make sure, that
+      entries in the given batch are set correctly.
+
+   .. method:: createBatch(creation_date, creator, batch_prefix, cost, entry_num)
+
+      Create a batch inside this container. Returns the batch created.
+
+      :param creation_date: python datetime
+      :param creator: creators user id
+      :type creator: string
+      :param batch_prefix: prefix of this batch
+      :param cost: cost per access code
+      :type cost: float
+      :param entry_num: number of access codes to create
+
+   .. method:: getNum(prefix)
+
+      Get next unused num for a new batch and a given prefix.
+
+      Batches for a given prefix are numerated. Whenever a new batch
+      is created and other batches inside the container already have
+      the same prefix, the new one will get the lowest unused number.
+
+   .. method:: getImportFiles()
+
+      Get a list of basenames of available import files, suitable for
+      feeding :meth:`reimport`.
+
+   .. method:: reimport(filename, creator=u'UNKNOWN')
+
+      Reimport a CSV log file of previously created AC batch.
+
+      ``filename`` is the name (basename) of the file residing in the
+      accesscode storage's ``import`` directory.``creator`` is the
+      user ID of the current user.
+
+   .. method:: search(search_term, search_type)
+
+      Look in all contained batches for access codes that comply with
+      the given parameters.
+
+      ``search_type`` must be one of ``serial``, ``pin``, or
+      ``stud_id`` and specifies, what kind of data is contained in the
+      ``search_term``.
+
+      - ``serial`` looks for the AC with the ``serial_batch`` set to
+        the given (integer) number in ``search_term``. Here
+        ``search_term`` can be an integer or a string. If it is a
+        string it will be converted to an int.
+
+      - ``pin`` looks for the AC with the ``representation`` set to
+        the given string in ``search_term``. Here ``search_term`` must
+        be a string containing the full AC-ID like
+        ``APP-1-0123456789``.
+
+      - ``stud_id`` looks for the AC with the ``student_id`` set to
+        the given string in ``search_term``. Here ``search_term`` must
+        be a string containing the full student ID.
+
+Examples:
+---------
+
+Creating a batch container:
+
+    >>> from waeup.kofa.accesscodes.accesscodes import (
+    ...   AccessCodeBatchContainer)
+    >>> container = AccessCodeBatchContainer()
+
+Creating batches inside the container:
+
+    >>> from datetime import datetime
+    >>> batch1 = container.createBatch(
+    ...   datetime.now(), 'some userid', 'FOO', 10.12, 5)
+
+    >>> batch2 = container.createBatch(
+    ...   datetime.now(), 'other userid', 'BAR', 1.95, 5)
+
+Searching the container for batch serials:
+
+    >>> container.search(1, 'serial')
+    [<waeup.kofa...AccessCode object at 0x...>,
+     <waeup.kofa...AccessCode object at 0x...>]
+
+    >>> container.search('not-a-number', 'serial')
+    []
+
+    >>> result = container.search('1', 'serial')
+    >>> result
+    [<waeup.kofa...AccessCode object at 0x...>,
+     <waeup.kofa...AccessCode object at 0x...>]
+
+Searching for ACs:
+
+    >>> ac = result[0]
+    >>> container.search(ac.representation, 'pin')
+    [<waeup.kofa...AccessCode object at 0x...>]
+
+Searching for student IDs:
+
+#    >>> ac.__parent__.invalidate(
+#    ...   ac.representation, 'some_user')
+#    >>> container.search('some_user', 'stud_id')
+#    [<waeup.kofa...AccessCode object at 0x...>]
+
+
+Access code plugin
+==================
+
+.. class:: AccessCodePlugin
+
+  A `waeup.kofa` plugin that updates existing Kofa university
+  instances so that they provide support for access-codes.
+
+  .. attribute:: grok.implements(IKofaPluggable)
+  .. attribute:: grok.name('accesscodes')
+
+  .. method:: setup(site, name, logger)
+
+     Create an accesscodebatch container in ``site``. Any events are
+     logged to ``logger``.
+
+  .. method:: update(site, name, logger)
+
+     Check if ``site`` contains an accesscodebatch container and add
+     it if missing. Any events are logged to ``logger``.
+
+  The AccessCodePlugin is available as a global named utility for the
+  IKofaPluggable interface named ``accesscodes``.
+
+  It is looked up by a university instance when created, so that this
+  instance has nothing to know about accesscodes at all and can
+  although provide support for them.
+
+    >>> from zope.component import getUtility
+    >>> from waeup.kofa.interfaces import IKofaPluggable
+    >>> plugin = getUtility(IKofaPluggable, name='accesscodes')
+    >>> plugin
+    <waeup.kofa.accesscodes.accesscodes.AccessCodePlugin object at 0x...>
+
+  It provides a `setup()` and an `update()` method. Both have to be
+  fed with a site object (the `IUniversity` instance to modify), a
+  logger and a name.
+
+  We create a faked site and a logger:
+
+    >>> import logging
+    >>> faked_site = dict()
+    >>> logger = logging.getLogger('ac_test')
+    >>> logger.setLevel(logging.DEBUG)
+    >>> ch = logging.FileHandler('ac_tests.log', 'w')
+    >>> ch.setLevel(logging.DEBUG)
+    >>> logger.addHandler(ch)
+
+  Now we can install our stuff in the faked site:
+
+    >>> plugin.setup(faked_site, 'blah', logger)
+
+  The faked site now has an access code container:
+
+    >>> faked_site.keys()
+    ['accesscodes']
+
+  The action is described in the log:
+
+    >>> print open('ac_tests.log', 'r').read()
+    Installed container for access code batches.
+
+  We can also update an existing site, by calling `update()`:
+
+    >>> plugin.update(faked_site, 'blah', logger)
+
+  There was nothing to do for the updater:
+
+    >>> print open('ac_tests.log', 'r').read()
+    Installed container for access code batches.
+    AccessCodePlugin: Updating site at {'accesscodes'...: Nothing to do.
+
+  But if we remove the created batch container and call the updater, it
+  will create a new one:
+
+    >>> del faked_site['accesscodes']
+    >>> plugin.update(faked_site, 'blah', logger)
+    >>> print open('ac_tests.log', 'r').read()
+    Installed container for access code batches.
+    AccessCodePlugin: Updating site at {'accesscodes'...: Nothing to do.
+    Updating site at {}. Installing access codes.
+    Installed container for access code batches.
+
+  Clean up:
+
+    >>> import os
+    >>> os.unlink('ac_tests.log')
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/api.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/api.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/api.txt	(revision 10886)
@@ -0,0 +1,17 @@
+:mod:`waeup.kofa.accesscodes` -- Access Codes
+*********************************************
+
+.. module:: waeup.kofa.accesscodes
+
+Components that represent access codes and related components.
+
+Submodules
+==========
+
+The :mod:`waeup.kofa.accesscodes` module provides the following submodules:
+
+.. toctree::
+   :maxdepth: 1
+
+   browser.txt
+   accesscodes.txt
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/batching.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/batching.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/batching.py	(revision 10886)
@@ -0,0 +1,224 @@
+## $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):
+    """A batch processor for IAccessCodeBatch objects.
+    """
+    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):
+    """A batch processor for IAccessCode objects.
+    """
+    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)
+        obj.batch_serial = row['batch_serial']
+        obj.random_num = row['random_num']
+        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/tags/0.2/src/waeup/kofa/accesscodes/browser.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/browser.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/browser.py	(revision 10886)
@@ -0,0 +1,263 @@
+## $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.resources import datatable
+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.'))
+            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.'))
+        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.'))
+            self.redirect(self.url(self.context))
+            return
+        if reimport is None:
+            return
+        if not filenames:
+            self.flash(_('No file chosen. Action cancelled.'))
+            self.redirect(self.url(self.context))
+            return
+        if isinstance(filenames, basestring):
+            filenames = [filenames]
+        userid = self.request.principal.id
+        for filename in filenames:
+            try:
+                self.context.reimport(filename, userid)
+            except KeyError:
+                self.flash(_('This batch already exists: ${a}',
+                    mapping = {'a':filename}))
+                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')
+
+    def update(self, *args, **kw):
+        datatable.need()
+        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:
+            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}))
+            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}))
+        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/tags/0.2/src/waeup/kofa/accesscodes/browser.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/browser.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/browser.txt	(revision 10886)
@@ -0,0 +1,448 @@
+:mod:`waeup.kofa.accesscodes.browser` -- 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.
+
+:NOTTest-Layer: functional
+
+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('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('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('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('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('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/tags/0.2/src/waeup/kofa/accesscodes/browser_templates/admintaskacbatches.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/browser_templates/admintaskacbatches.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/browser_templates/admintaskacbatches.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/accesscodes/browser_templates/batchcontainer.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/browser_templates/batchcontainer.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/browser_templates/batchcontainer.pt	(revision 10886)
@@ -0,0 +1,54 @@
+<form i18n:domain="waeup.kofa" method="POST">
+  <p i18n:translate="">
+    The following batches are available:
+  </p>
+  <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" name="archive"
+      tal:attributes="value view/archive_button" />
+  <input type="submit" class="btn" name="delete"
+       tal:attributes="value view/delete_button" />
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt	(revision 10886)
@@ -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>
+    <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 primary" name="reimport"
+      tal:attributes="value view/reimport_button" />
+  <input type="submit" class="btn" name="cancel"
+      tal:attributes="value view/cancel_button" />
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/browser_templates/searchpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/browser_templates/searchpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/browser_templates/searchpage.pt	(revision 10886)
@@ -0,0 +1,52 @@
+<form method="POST" i18n:domain="waeup.kofa">
+<br />
+  <input type="submit" class="btn primary" name="search"
+      tal:attributes="value view/search_button" />
+  <span i18n:translate="">for access codes</span>
+  <select name="searchtype">
+    <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>
+  <input type="text" name="searchterm" />
+  <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="display 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" name="disable"
+          tal:attributes="value view/disable_button" />
+      <input type="submit" class="btn" name="enable"
+          tal:attributes="value view/enable_button" />
+    </div>
+
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/catalog.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/catalog.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/catalog.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/accesscodes/export.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/export.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/export.py	(revision 10886)
@@ -0,0 +1,91 @@
+## $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):
+    """Exporter for accesscodebatches.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('accesscodebatches')
+
+    #: Fieldnames considered by this exporter
+    fields = tuple(sorted(iface_names(IAccessCodeBatch))) + ('batch_id',)
+
+    #: The title under which this exporter will be displayed
+    title = _(u'AccessCodeBatches')
+
+    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):
+    """Exporter for courses.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('accesscodes')
+
+    #: Fieldnames considered by this exporter
+    fields = tuple(sorted(iface_names(IAccessCode)))
+
+    #: The title under which this exporter will be displayed
+    title = _(u'AccessCodes')
+
+    def mangle_value(self, value, name, context=None):
+        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/tags/0.2/src/waeup/kofa/accesscodes/interfaces.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/interfaces.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/interfaces.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/accesscodes/tests/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/__init__.py	(revision 10886)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/sample_ac_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/sample_ac_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/sample_ac_data.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/accesscodes/tests/sample_ac_data_update.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/sample_ac_data_update.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/sample_ac_data_update.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/accesscodes/tests/sample_batch_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/sample_batch_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/sample_batch_data.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/accesscodes/tests/sample_import.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/sample_import.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/sample_import.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/accesscodes/tests/test_accesscode.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/test_accesscode.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/test_accesscode.py	(revision 10886)
@@ -0,0 +1,476 @@
+## $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 [
+        #'accesscodes.txt',
+        '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/tags/0.2/src/waeup/kofa/accesscodes/tests/test_batching.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/test_batching.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/test_batching.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/accesscodes/tests/test_catalog.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/test_catalog.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/test_catalog.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/accesscodes/tests/test_export.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/test_export.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/test_export.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/accesscodes/tests/test_workflow.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/test_workflow.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/tests/test_workflow.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/accesscodes/workflow.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/workflow.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/accesscodes/workflow.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/app.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/app.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/app.py	(revision 10886)
@@ -0,0 +1,101 @@
+## $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 all plugins and call their `update()` method.
+        """
+        name = getattr(self, '__name__', '<Unnamed>')
+        self.logger.info('Fire upgrade event for site %s' % name)
+        grok.notify(ObjectUpgradeEvent(self))
+        self.logger.info('Done.')
+        self.logger.info('Now upgrading any plugins.')
+        for name, plugin in getUtilitiesFor(IKofaPluggable):
+            plugin.update(self, name, self.logger)
+        self.logger.info('Plugin update finished.')
+        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):
+    app.logger.info('University `%s` added.' % getattr(app, '__name__', None))
+    return
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/app.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/app.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/app.txt	(revision 10886)
@@ -0,0 +1,146 @@
+:mod:`waeup.kofa.app` -- central components for Kofa
+****************************************************
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+.. module:: waeup.kofa.app
+
+.. class:: University
+
+  The main Kofa application object is given with
+  :class:`University`. It provides the main containers as faculties,
+  hostels, etc.
+
+  .. attribute:: name
+
+     A string. The name of a university.
+
+  .. attribute:: faculties
+
+     An arbitrary object containing "faculties". In the case of
+     `University` it is a container of type
+     `waeup.kofa.interfaces.IFacultiesContainer`.
+
+
+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...>
+
+We can export universities. For this we lookup an appropriate exporter
+first::
+
+  >>> from waeup.kofa.interfaces import IKofaExporter
+  >>> exporter = IKofaExporter(myuniversity)
+  >>> exporter
+  <waeup.kofa.utils.importexport.Exporter object at 0x...>
+
+Now we can trigger the export::
+
+  >>> exporter.export()
+  <cStringIO.StringO object at 0x...>
+
+We can also get an XML representation as file. If we do not provide a
+filepath, we will get an instance of `cStringIO.StringIO`, i.e. a
+memory file::
+
+  >>> from waeup.kofa.interfaces import IKofaXMLExporter
+  >>> exporter = IKofaXMLExporter(myuniversity)
+  >>> f = exporter.export()
+  >>> f
+  <cStringIO.StringO object at 0x...>
+
+  >>> print f.read()
+  <?xml version="1.0" encoding="utf-8" ?>
+  <pickle>
+    <initialized_object id="...">
+  ...
+  </pickle>
+
+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'accesscodes', <waeup.kofa.accesscodes...AccessCodePlugin ...)]
+
+
+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/tags/0.2/src/waeup/kofa/applicants/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/__init__.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/applicant.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/applicant.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/applicant.py	(revision 10886)
@@ -0,0 +1,380 @@
+## $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 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)
+from waeup.kofa.applicants.workflow import application_states_dict
+
+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)
+
+    create_names = [
+        'firstname', 'middlename', 'lastname',
+        'sex', 'date_of_birth',
+        'email', 'phone'
+        ]
+
+    def __init__(self):
+        super(Applicant, self).__init__()
+        self.password = None
+        self.application_date = None
+        self.applicant_id = None
+        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.applicant_id, message))
+        return
+
+    @property
+    def state(self):
+        return IWorkflowState(self).getState()
+
+    @property
+    def container_code(self):
+        return self.__parent__.code
+
+    @property
+    def translated_state(self):
+        return application_states_dict[self.state]
+
+    @property
+    def history(self):
+        history = IObjectHistory(self)
+        return history
+
+    @property
+    def special(self):
+        return self.__parent__.prefix.startswith('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
+        and copy applicant data.
+        """
+        # 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:
+            return False, _('Registration Number exists.')
+        # Has the course_admitted field been properly filled?
+        if self.course_admitted is None:
+            return False, _('No course admitted provided.')
+        # Set student attributes
+        try:
+            for name in self.create_names:
+                setattr(student, name, getattr(self, name, None))
+        except RequiredMissing, err:
+            return False, 'RequiredMissing: %s' % err
+        except:
+            return False, 'Unknown Error'
+        # Finally prove if the certificate still exists
+        try:
+            StudentStudyCourse().certificate = self.course_admitted
+        except ConstraintNotSatisfied, err:
+            return False, 'ConstraintNotSatisfied: %s' % self.course_admitted.code
+        # Add student
+        site = grok.getSite()
+        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)
+        # Update the catalog
+        notify(grok.ObjectModifiedEvent(student))
+        # Save application slip
+        self._createApplicationPDF(student, view=view)
+
+        return True, _('Student ${a} created', mapping = {'a':student.student_id})
+
+    def _createApplicationPDF(self, student, view=None):
+        """Create an application slip as PDF and store it in student folder.
+        """
+        file_store = getUtility(IExtFileStore)
+        applicant_slip = getAdapter(self, IPDF, name='application_slip')(
+            view=view)
+        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
+
+# 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')
+
+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 FUTMinna requires
+        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.
+    """
+    comment = 'Applicant 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 also any passport image.
+    file_store = getUtility(IExtFileStore)
+    file_store.deleteFileByContext(applicant)
+    return
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/authentication.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/authentication.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/authentication.py	(revision 10886)
@@ -0,0 +1,118 @@
+## $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 student account has been suspended.
+        """
+        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/tags/0.2/src/waeup/kofa/applicants/batching.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/batching.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/batching.py	(revision 10886)
@@ -0,0 +1,342 @@
+## $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 zope.schema import getFields
+from zope.interface import Interface
+from zope.component import queryUtility
+from hurry.workflow.interfaces import IWorkflowState
+from zope.catalog.interfaces import ICatalog
+from waeup.kofa.interfaces import (
+    IBatchProcessor, IObjectConverter, FatalCSVError, IGNORE_MARKER,
+    IObjectHistory, IUserAccount)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.batching import BatchProcessor
+from waeup.kofa.applicants.interfaces import (
+    IApplicantsContainer, IApplicant, IApplicantUpdateByRegNo)
+from waeup.kofa.applicants.workflow import  IMPORTABLE_STATES, CREATED
+
+class ApplicantsContainerProcessor(BatchProcessor):
+    """A processor for applicants containers.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'applicantscontainerprocessor'
+    grok.name(util_name)
+
+    name = u'Applicants Container 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):
+    """A batch processor for IApplicant objects.
+
+    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 searched by its applicant_id
+    or reg_number.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'applicantprocessor'
+    grok.name(util_name)
+    name = u'Applicant Processor'
+    iface = IApplicant
+    iface_byregnumber = IApplicantUpdateByRegNo
+    location_fields = ['']
+    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 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
+                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.
+            parent = self.getParent(row, self.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
+
+    def doImport(self, *args, **kw):
+        # XXX: Not thread-safe.  Parallel applicant imports into
+        # different sites could mean a mess.  Luckily this is not a
+        # typical use-case. On the other hand it spares thousands of
+        # site lookups during large imports.
+        # XXX: Maybe this should go into Importer base.
+        self.site = grok.getSite() # needed by checkConversion()
+        return super(ApplicantProcessor, self).doImport(*args, **kw)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser.py	(revision 10886)
@@ -0,0 +1,1256 @@
+## $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
+from datetime import datetime, date
+from zope.event import notify
+from zope.component import getUtility, createObject, getAdapter
+from zope.catalog.interfaces import ICatalog
+from zope.i18n import translate
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from waeup.kofa.applicants.interfaces import (
+    IApplicant, IApplicantEdit, IApplicantsRoot,
+    IApplicantsContainer, IApplicantsContainerAdd,
+    MAX_UPLOAD_SIZE, IApplicantOnlinePayment, IApplicantsUtils,
+    IApplicantRegisterUpdate, ISpecialApplicant
+    )
+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)
+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.resources import toggleall
+from waeup.kofa.browser.layout import (
+    NullValidator, jsaction, action, UtilityView, JSAction)
+from waeup.kofa.browser.pages import (
+    add_local_role, del_local_roles, doll_up, ExportCSVView)
+from waeup.kofa.browser.resources import datepicker, tabs, datatable, warning
+from waeup.kofa.interfaces import (
+    IKofaObject, ILocalRolesAssignable, IExtFileStore, IPDF,
+    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)
+from waeup.kofa.widgets.htmlwidget import HTMLDisplayWidget
+
+grok.context(IKofaObject) # Make IKofaObject the default context
+
+class SubmitJSAction(JSAction):
+
+    msg = _('\'You can not edit your application records after final submission.'
+            ' You really want to submit?\'')
+
+class submitaction(grok.action):
+
+    def __call__(self, success):
+        action = SubmitJSAction(self.label, success=success, **self.options)
+        self.actions.append(action)
+        return action
+
+class ApplicantsRootPage(KofaDisplayFormPage):
+    grok.context(IApplicantsRoot)
+    grok.name('index')
+    grok.require('waeup.Public')
+    form_fields = grok.AutoFields(IApplicantsRoot)
+    form_fields['description'].custom_widget = HTMLDisplayWidget
+    label = _('Application 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()
+        return [value for value in self.context.values() if not value.hidden]
+
+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):
+        datatable.need()
+        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'))
+            return
+        self.results = search(query=self.searchterm,
+            searchtype=self.searchtype, view=self)
+        if not self.results:
+            self.flash(_('No applicant found.'))
+        return
+
+class ApplicantsRootManageFormPage(KofaEditFormPage):
+    grok.context(IApplicantsRoot)
+    grok.name('manage')
+    grok.template('applicantsrootmanagepage')
+    form_fields = grok.AutoFields(IApplicantsRoot)
+    label = _('Manage application 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')
+
+    def update(self):
+        tabs.need()
+        datatable.need()
+        warning.need()
+        self.tab1 = self.tab2 = self.tab3 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        return super(ApplicantsRootManageFormPage, self).update()
+
+    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 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!'))
+            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]))
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping = {'a':', '.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)
+
+    def _description(self):
+        view = ApplicantsRootPage(
+            self.context,self.request)
+        view.setUpWidgets()
+        return view.widgets['description']()
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        self.context.description_dict = self._description()
+        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')
+
+    def update(self):
+        datepicker.need() # Enable jQuery datepicker in date fields.
+        return super(ApplicantsContainerAddFormPage, self).update()
+
+    @action(_('Add applicants container'))
+    def addApplicantsContainer(self, **data):
+        year = data['year']
+        code = u'%s%s' % (data['prefix'], year)
+        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.'))
+            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)
+        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 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')
+        form_fields['description'].custom_widget = HTMLDisplayWidget
+        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')
+        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')
+
+    @property
+    def label(self):
+        return _('Manage applicants container')
+
+    pnav = 3
+
+    @property
+    def showApplicants(self):
+        if len(self.context) < 5000:
+            return True
+        return False
+
+    def update(self):
+        datepicker.need() # Enable jQuery datepicker in date fields.
+        tabs.need()
+        toggleall.need()
+        self.tab1 = self.tab2 = self.tab3 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        warning.need()
+        datatable.need()  # Enable jQurey datatables for contents listing
+        return super(ApplicantsContainerManageFormPage, self).update()
+
+    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 getUsersWithLocalRoles(self):
+        return get_users_with_local_roles(self.context)
+
+    def _description(self):
+        view = ApplicantsContainerPage(
+            self.context,self.request)
+        view.setUpWidgets()
+        return view.widgets['description']()
+
+    @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 = []
+        self.context.description_dict = self._description()
+        # Always refresh title. So we can change titles
+        # if APP_TYPES_DICT has been edited.
+        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
+        title = apptypes_dict[self.context.prefix][0]
+        #self.context.title = u'%s %s/%s' % (
+        #    title, self.context.year, self.context.year + 1)
+        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!'))
+            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]))
+        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):
+        form = self.request.form
+        if 'val_id' in form:
+            child_id = form['val_id']
+        else:
+            self.flash(_('No applicant selected!'))
+            self.redirect(self.url(self.context, '@@manage')+'?tab2')
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        created = []
+        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.'))
+        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
+
+    @action(_('Create application record'))
+    def addApplicant(self, **data):
+        applicant = createObject(u'waeup.Applicant')
+        self.applyData(applicant, **data)
+        self.context.addApplicant(applicant)
+        self.flash(_('Applicant record created.'))
+        self.redirect(
+            self.url(self.context[applicant.application_number], 'index'))
+        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 form_fields(self):
+        if self.context.special:
+            form_fields = grok.AutoFields(ISpecialApplicant)
+        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')
+        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')
+    form_fields = grok.AutoFields(IApplicant).select(
+        'applicant_id','email', 'course1')
+
+class CreateStudentPage(UtilityView, grok.View):
+    """Create a student object from applicant data.
+    """
+    grok.context(IApplicant)
+    grok.name('createstudent')
+    grok.require('waeup.manageStudent')
+
+    def update(self):
+        msg = self.context.createStudent(view=self)[1]
+        self.flash(msg)
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class CreateAllStudentsPage(UtilityView, grok.View):
+    """Create all student objects from applicant data
+    in a container.
+
+    This is a hidden page, no link or button will
+    be provided and only PortalManagers can do this. 
+    """
+    #grok.context(IApplicantsContainer)
+    grok.name('createallstudents')
+    grok.require('waeup.managePortal')
+
+    def update(self):
+        cat = getUtility(ICatalog, name='applicants_catalog')
+        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
+        created = []
+        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:
+                ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+                applicants_root.logger.info(
+                    '%s - %s - %s' % (ob_class, result.applicant_id, msg))
+        if len(created):
+            self.flash(_('${a} students successfully created.',
+                mapping = {'a': len(created)}))
+        else:
+            self.flash(_('No student could be created.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        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'
+
+    def update(self):
+        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.'))
+                  self.redirect(self.url(self.context))
+                  return
+        applicants_utils = getUtility(IApplicantsUtils)
+        container = self.context.__parent__
+        payment = createObject(self.factory)
+        error = applicants_utils.setPaymentDetails(
+            container, payment, self.context)
+        if error is not None:
+            self.flash(error)
+            self.redirect(self.url(self.context))
+            return
+        self.context[payment.p_id] = payment
+        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):
+        success, 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)
+        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
+
+    def render(self):
+        #if self.context.p_state != 'paid':
+        #    self.flash(_('Ticket not yet paid.'))
+        #    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.'))
+            return self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
+            view=self)
+        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.
+    """
+    size = file_size(upload)
+    if size > MAX_UPLOAD_SIZE:
+        view.flash(_('Uploaded image is too big!'))
+        return False
+    dummy, ext = os.path.splitext(upload.filename)
+    ext.lower()
+    if ext != '.jpg':
+        view.flash(_('jpg file extension expected.'))
+        return False
+    upload.seek(0) # file pointer moved when determining size
+    store = getUtility(IExtFileStore)
+    file_id = IFileStoreNameChooser(context).chooseName()
+    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'), _('Final Submit')],
+        [_('Add online payment ticket'),_('Remove selected tickets')]]
+
+    @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
+
+    def update(self):
+        datepicker.need() # Enable jQuery datepicker in date fields.
+        warning.need()
+        super(ApplicantManageFormPage, self).update()
+        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:
+            # 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')
+        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] == 'pay']
+        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))
+                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.'))
+            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]))
+        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'))
+    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 form_fields(self):
+        if self.context.special:
+            form_fields = grok.AutoFields(ISpecialApplicant)
+            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()
+        actions = [[],[]]
+        if state == STARTED:
+            actions = [[_('Save')],
+                [_('Add online payment ticket'),_('Remove selected tickets')]]
+        elif state == PAID:
+            actions = [[_('Save'), _('Final Submit')],
+                [_('Remove selected tickets')]]
+        return actions
+
+    def unremovable(self, ticket):
+        state = IWorkflowState(self.context).getState()
+        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
+
+    def emit_lock_message(self):
+        self.flash(_('The requested form is locked (read-only).'))
+        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):
+        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'))
+    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
+        if data.get('course1', 1) == data.get('course2', 2):
+            self.flash(_('1st and 2nd choice must be different.'))
+            return
+        self.applyData(self.context, **data)
+        self.flash(_('Form has been saved.'))
+        return
+
+    @submitaction(_('Final Submit'))
+    def finalsubmit(self, **data):
+        if self.upload_success is False:  # False is not None!
+            return # error during image upload. Ignore other values
+        if self.dataNotComplete():
+            self.flash(self.dataNotComplete())
+            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!'))
+            return
+        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(_('Form has been submitted.'))
+        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(
+                'firstname','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.'))
+            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
+
+    @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':
+            # Add applicant
+            applicant = createObject(u'waeup.Applicant')
+            self.applyData(applicant, **data)
+            self.context.addApplicant(applicant)
+            applicant.reg_number = applicant.applicant_id
+            notify(grok.ObjectModifiedEvent(applicant))
+        elif self.context.mode == 'update':
+            # Update applicant
+            reg_number = data.get('reg_number','')
+            firstname = data.get('firstname','')
+            cat = getUtility(ICatalog, name='applicants_catalog')
+            results = list(
+                cat.searchResults(reg_number=(reg_number, reg_number)))
+            if results:
+                applicant = results[0]
+                if getattr(applicant,'firstname',None) is None:
+                    self.flash(_('An error occurred.'))
+                    return
+                elif applicant.firstname.lower() != firstname.lower():
+                    # Don't tell the truth here. Anonymous must not
+                    # know that a record was found and only the firstname
+                    # verification failed.
+                    self.flash(_('No application record found.'))
+                    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.'))
+                    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.'))
+                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 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 = _('Applicant 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(KofaPage):
+    """Page that starts an applicants export job.
+
+    """
+    grok.context(VirtualApplicantsExportJobContainer)
+    grok.require('waeup.manageApplication')
+    grok.name('start_export')
+
+    def update(self):
+        exporter = 'applicants'
+        container_code = self.context.__parent__.code
+        job_id = self.context.start_export_job(exporter,
+                                      self.request.principal.id,
+                                      container=container_code)
+
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.logger.info(
+            '%s - exported: %s (%s), job_id=%s'
+            % (ob_class, exporter, container_code, job_id))
+        self.flash(_('Export 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')
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt	(revision 10886)
@@ -0,0 +1,33 @@
+<tal:statistics define="stats context/statistics" i18n:domain="waeup.kofa">
+  <h2 i18n:translate="">Container Statistics</h2>
+  <table class="span8">
+    <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','admitted','not admitted',
+          'created')">
+        <td tal:content="state">STATE</td>
+        <td tal:content="python: stats[0][state] ">NUMBER</td>
+      </tr>
+    </tbody>
+  </table>
+  <table class="span8" 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/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt	(revision 10886)
@@ -0,0 +1,65 @@
+<h2 i18n:domain="waeup.kofa">
+  ...
+  <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">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
+
+<img src="" height="180px" tal:attributes="src view/passport_url" />
+
+<table i18n:domain="waeup.kofa" class="form-table">
+  <tbody>
+    <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>
+<h3  i18n:domain="waeup.kofa" i18n:translate="">
+    Application Fee Payment Tickets
+</h3>
+<table i18n:domain="waeup.kofa">
+  <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/values">
+      <td> <a tal:attributes="href value/__name__">
+      <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>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicanteditpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicanteditpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicanteditpage.pt	(revision 10886)
@@ -0,0 +1,124 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data"
+      autocomplete="off">
+
+  <h2 i18n:domain="waeup.kofa">
+    ...
+    <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">
+    <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>
+        <td class="fieldname" i18n:translate="">
+          Passport Photo:
+        </td>
+        <td>
+          <img src="passport.jpg" height="180px" /><br />
+          <input type="file" name="form.passport" />
+          <br />
+          <span i18n:translate="">
+            Max. file size:
+          </span>
+          <span tal:replace="view/max_upload_size">10 KB</span>
+      </tr>
+      <tr tal:condition="view/manage_applications">
+        <td class="fieldname" i18n:translate="">Password:</td>
+        <td>
+          <input name="password" type="password"  />
+        </td>
+      </tr>
+      <tr tal:condition="view/manage_applications">
+        <td class="fieldname" i18n:translate="">Retype Password:</td>
+        <td>
+          <input 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">
+            <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="not: view/manage_applications">
+    <input id="confirm_passport" name="confirm_passport"
+           type="checkbox" value="True"/>
+    <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>
+
+  <br /><br />
+  <h3 i18n:translate="">
+      Application Fee Payment Tickets
+  </h3>
+
+  <table>
+    <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/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 cl/__name__">
+        <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>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantregemailsent.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantregemailsent.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantregemailsent.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantregister.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantregister.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantregister.pt	(revision 10886)
@@ -0,0 +1,31 @@
+<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" />
+      <tr tal:condition="view/captcha_code">
+        <td i18n:translate="">
+          Please solve the captcha<br />to prevent misuse of this service:
+        </td>
+        <td>
+          <div tal:content="structure view/captcha_code"></div>
+        </td>
+      </tr>
+
+    </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:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantscontaineraddpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantscontaineraddpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantscontaineraddpage.pt	(revision 10886)
@@ -0,0 +1,15 @@
+<form action="." tal:attributes="action request/URL" method="post"
+  class="edit-form" 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/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt	(revision 10886)
@@ -0,0 +1,158 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+  i18n:domain="waeup.kofa" class="edit-form" enctype="multipart/form-data">
+
+  <ul class="tabs" data-tabs="tabs">
+    <li tal:attributes="class view/tab1">
+    <a href="#tab-1">
+      <span i18n:translate="">Settings
+      </span></a>
+    </li>
+    <li tal:attributes="class view/tab2">
+    <a href="#tab-2">
+      <span i18n:translate="">Applicants
+      </span></a>
+    </li>
+    <li tal:attributes="class view/tab3">
+    <a href="#tab-3">
+      <span i18n:translate="">Local Roles
+      </span></a>
+    </li>
+  </ul>
+  <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+      <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="tab-2" tal:attributes="class view/tab2">
+    <tal:showApplicants  condition="view/showApplicants">
+    <input type="checkbox" onClick="toggle(this, 'val_id')" />
+    <span  i18n:translate="">Select all</span>
+    <br /><br />
+
+      <table class="display 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 class="gradeC" tal:repeat="appl context/values">
+            <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>
+      <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 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
+          <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="tab-3" tal:attributes="class view/tab3"> <br />
+      <table class="display 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" class="gradeC"> <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">
+              <option tal:repeat="user view/getUsers"
+                      tal:attributes="value user/name">
+              <span tal:replace="user/val/title">USERTITLE
+              </span>
+              </option>
+            </select> </td> <td>
+            <select 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> </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/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantscontainerpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantscontainerpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantscontainerpage.pt	(revision 10886)
@@ -0,0 +1,20 @@
+<table class="form-table">
+  <tbody>
+    <tal:block repeat="widget view/widgets">
+      <tal:condition condition="python:widget.name != 'form.description'">
+      <tr>
+        <td class="fieldname">
+          <tal:block replace="python:widget.label"/>:
+        </td>
+        <td>
+          <tal:block tal:replace="structure widget" />
+        </td>
+      </tr>
+      </tal:condition>
+    </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/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt	(revision 10886)
@@ -0,0 +1,137 @@
+<form action="." tal:attributes="action request/URL"
+  i18n:domain="waeup.kofa" method="POST"  enctype="multipart/form-data">
+  <ul class="tabs" data-tabs="tabs">
+    <li tal:attributes="class view/tab1">
+    <a href="#tab-1">
+      <span i18n:translate="">Introduction
+      </span></a>
+    </li>
+    <li tal:attributes="class view/tab2">
+    <a href="#tab-2">
+      <span tal:content="view/subunits">Contents
+      </span></a>
+    </li>
+    <li tal:attributes="class view/tab3">
+    <a href="#tab-3">
+      <span i18n:translate="">Local Roles
+      </span></a>
+    </li>
+  </ul>
+  <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+      <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="tab-2" tal:attributes="class view/tab2">
+      <h3 tal:content="view/subunits">SUBUNITS</h3>
+
+      <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="display 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>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry context/values" 	class="gradeC"> <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)">YEAR</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="tab-3" tal:attributes="class view/tab3">
+      <br />
+      <table class="display 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" class="gradeC">
+            <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">
+              <option tal:repeat="user view/getUsers"
+                tal:attributes="value user/name">
+              <span tal:replace="user/val/title">USERTITLE
+              </span>
+              </option>
+            </select> </td> <td>
+            <select 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> </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/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantsrootpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantsrootpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantsrootpage.pt	(revision 10886)
@@ -0,0 +1,33 @@
+<span tal:condition="view/introduction" tal:omit-tag="">
+  <span tal:content="structure view/introduction">INTRODUCTION</span>
+  <br />
+</span>
+
+<table i18n:domain="waeup.kofa">
+  <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" class="gradeC">
+      <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>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantssearchpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantssearchpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/applicantssearchpage.pt	(revision 10886)
@@ -0,0 +1,46 @@
+<form method="POST" i18n:domain="waeup.kofa">
+<br />
+  <input class="btn primary" type="submit" name="search"
+    tal:attributes="value view/search_button" />
+
+  <select name="searchtype">
+    <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>
+
+  <input type="text" name="searchterm" />
+
+  <p>&nbsp;</p>
+  <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="display 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" class="gradeC">
+        <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>
+    <br />
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/exportjobsindex.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/exportjobsindex.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/browser_templates/exportjobsindex.pt	(revision 10886)
@@ -0,0 +1,62 @@
+<table i18n:domain="waeup.kofa">
+  <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 nowrap>
+        <span tal:replace="job/start_time">DATETIME</span>
+      </td>
+      <td>
+        <span tal:replace="job/status">STATUS</span>
+      </td>
+      <td nowrap>
+        <a href="" class="btn" i18n:translate=""
+           tal:condition="job/show_refresh_button">
+          <img tal:attributes="src static/actionicon_reload.png" />
+          Reload
+        </a>
+        <form method="POST">
+          <a href="" class="btn primary small"
+                 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 small"
+                 name="DISCARD" value="Discard"
+                 tal:condition="job/show_discard_button" />
+        </form>
+      </td>
+    </tr>
+  </tbody>
+</table>
+<form method="POST" i18n:domain="waeup.kofa">
+  <div class="span12">
+
+  </div>
+  <input class="btn primary" type="submit" name="CREATE"
+         value="Start new export"
+          />
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/container.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/container.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/container.py	(revision 10886)
@@ -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
+##
+"""
+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
+from zope.component.factory import Factory
+from zope.component.interfaces import IFactory
+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 certificates.
+    """
+
+class ApplicantsContainer(grok.Container):
+    """An applicants container contains university applicants.
+    """
+    grok.implements(IApplicantsContainer,IApplicantsContainerAdd)
+
+    #: A dictionary to hold per language translations of description string.
+    description_dict = {}
+
+    local_roles = []
+
+    def archive(self, app_ids=None):
+        """Create on-dist archive of applicants stored in this term.
+
+        If app_ids is `None`, all applicants are archived.
+
+        If app_ids contains a single id string, only the respective
+        applicants are archived.
+
+        If app_ids contains a list of id strings all of the respective
+        applicants types are saved to disk.
+        """
+        raise NotImplementedError()
+
+    def clear(self, app_ids=None, archive=True):
+        """Remove applicants of type given by 'id'.
+
+        Optionally archive the applicants.
+
+        If id is `None`, all applicants are archived.
+
+        If id contains a single id string, only the respective
+        applicants are archived.
+
+        If id contains a list of id strings all of the respective
+        applicant types are saved to disk.
+
+        If `archive` is ``False`` none of the archive-handling is done
+        and respective applicants are simply removed from the
+        database.
+        """
+        raise NotImplementedError()
+
+    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 statistics(self):
+        return getUtility(IApplicantsUtils).getApplicantsStatistics(self)
+
+    @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 appropriate containers.
+        """
+        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/tags/0.2/src/waeup/kofa/applicants/dynamicroles.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/dynamicroles.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/dynamicroles.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/export.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/export.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/export.py	(revision 10886)
@@ -0,0 +1,125 @@
+## $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)
+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):
+    """Exporter for ApplicantsContainers.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('applicantscontainers')
+
+    #: Fieldnames considered by this exporter
+    fields = tuple(sorted(iface_names(IApplicantsContainer)))
+    #: The title under which this exporter will be displayed
+    title = _(u'Basic 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 ApplicantsExporter(grok.GlobalUtility, ExporterBase):
+    """Exporter for Applicants.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('applicants')
+
+    #: Fieldnames considered by this exporter
+    fields = tuple(sorted(IApplicantBaseData.names())) + ('container_code',)
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Applicants')
+
+    def mangle_value(self, value, name, context=None):
+        if name in (
+            'course1', 'course2', 'course_admitted') 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 = value.messages
+        elif name == 'phone' and value is not None:
+            # Append hash '#' to phone numbers to circumvent
+            # unwanted excel automatic
+            value = str('%s#' % value)
+        return super(
+            ApplicantsExporter, 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 applicants into filepath as CSV data.
+
+        If `filepath` is ``None``, a raw string with CSV data is returned.
+        """
+        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))
+        return self.export(applicants, filepath)
+
+    def export_filtered(self, site, filepath=None, **kw):
+        """Export applicants in container.
+
+        If `filepath` is ``None``, a raw string with CSV data should
+        be returned.
+        """
+        container = grok.getSite()['applicants'][kw['container']]
+        return self.export(container.values(), filepath=filepath)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/interfaces.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/interfaces.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/interfaces.py	(revision 10886)
@@ -0,0 +1,639 @@
+## $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.browser.interfaces import IApplicantBase
+from waeup.kofa.schema import TextLineChoice, FormattedDate
+from waeup.kofa.interfaces import (
+    IKofaObject, validate_email,
+    SimpleKofaVocabulary)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.payments.interfaces import IOnlinePayment
+from waeup.kofa.schema import PhoneNumber
+from waeup.kofa.students.vocabularies import GenderSource, RegNumberSource
+from waeup.kofa.university.vocabularies import (
+    AppCatSource, CertificateSource, SpecialApplicationSource)
+
+#: Maximum upload size for applicant passport photographs (in bytes)
+MAX_UPLOAD_SIZE = 1024 * 20
+
+_marker = object() # a marker different from None
+
+def year_range():
+    curr_year = datetime.now().year
+    return range(curr_year - 2, 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).filterCertificates(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 sorted(appcats_dict.keys())
+
+    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 university applicants containers.
+    """
+
+    description = schema.Text(
+        title = _(u'Human readable description in HTML format'),
+        required = False,
+        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.'''
+        )
+
+    description_dict = Attribute(
+        """Content as language dictionary with values in HTML format.""")
+
+class IApplicantsContainer(IKofaObject):
+    """An applicants container contains university applicants.
+
+    """
+
+    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,
+        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.'''
+        )
+
+    description_dict = Attribute(
+        """Content as language dictionary with values in HTML format.""")
+
+    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,
+        )
+
+    hidden= schema.Bool(
+        title = _(u'Hide container'),
+        required = False,
+        default = False,
+        )
+
+    def archive(id=None):
+        """Create on-dist archive of applicants stored in this term.
+
+        If id is `None`, all applicants are archived.
+
+        If id contains a single id string, only the respective
+        applicants are archived.
+
+        If id contains a list of id strings all of the respective
+        applicants types are saved to disk.
+        """
+
+    def clear(id=None, archive=True):
+        """Remove applicants of type given by 'id'.
+
+        Optionally archive the applicants.
+
+        If id is `None`, all applicants are archived.
+
+        If id contains a single id string, only the respective
+        applicants are archived.
+
+        If id contains a list of id strings all of the respective
+        applicant types are saved to disk.
+
+        If `archive` is ``False`` none of the archive-handling is done
+        and respective applicants are simply removed from the
+        database.
+        """
+
+    def writeLogMessage(view, comment):
+        """Adds an INFO message to the log file
+        """
+
+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,
+        )
+
+IApplicantsContainerAdd[
+    'prefix'].order =  IApplicantsContainer['prefix'].order
+IApplicantsContainerAdd[
+    'year'].order =  IApplicantsContainer['year'].order
+
+class IApplicantBaseData(IApplicantBase):
+    """The data for an applicant.
+
+    This is a base interface 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.
+    """
+
+    history = Attribute('Object history, a list of messages')
+    state = Attribute('The application state of an applicant')
+    display_fullname = Attribute('The fullname of an applicant')
+    application_date = Attribute('UTC datetime of submission, used for export only')
+    password = Attribute('Encrypted password of a applicant')
+    application_number = Attribute('The key under which the record is stored')
+
+    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,
+        )
+    #access_code = schema.TextLine(
+    #    title = u'Activation Code',
+    #    required = False,
+    #    readonly = True,
+    #    )
+    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,
+        #date_format = u'%d/%m/%Y', # Use grok-instance-wide default
+        show_year = True,
+        )
+    sex = schema.Choice(
+        title = _(u'Sex'),
+        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 = True,
+        )
+    course2 = schema.Choice(
+        title = _(u'2nd Choice Course of Study'),
+        source = AppCatCertificateSource(),
+        required = False,
+        )
+    #school_grades = schema.List(
+    #    title = _(u'School Grades'),
+    #    value_type = ResultEntryField(),
+    #    required = False,
+    #    default = [],
+    #    )
+
+    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 IApplicant(IApplicantBaseData):
+    """An applicant.
+
+    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):
+        """Adds an INFO message to the log file
+        """
+
+    def createStudent():
+        """Create a student object from applicatnt data
+        and copy applicant object.
+        """
+
+class ISpecialApplicant(IApplicantBase):
+    """A special applicant.
+
+    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.
+    """
+
+    history = Attribute('Object history, a list of messages')
+    state = Attribute('The application state of an applicant')
+    display_fullname = Attribute('The fullname of an applicant')
+    application_date = Attribute('UTC datetime of submission, used for export only')
+    password = Attribute('Encrypted password of a applicant')
+    application_number = Attribute('The key under which the record is stored')
+
+    suspended = schema.Bool(
+        title = _(u'Account suspended'),
+        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):
+    """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):
+    """Representation of an applicant.
+
+    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):
+    """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,
+        )
+
+    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.
+
+        """
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/payment.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/payment.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/payment.py	(revision 10886)
@@ -0,0 +1,126 @@
+## $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.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.
+        """
+        wf_info = IWorkflowInfo(self.__parent__)
+        try:
+            wf_info.fireTransition('approve')
+        except InvalidTransitionError:
+            msg = log = 'Error: %s' % sys.exc_info()[1]
+            return False, msg, log
+        log = 'payment approved: %s' % self.p_id
+        msg = _('Payment approved')
+        return False, msg, log
+
+    def doAfterApplicantPayment(self):
+        """Process applicant after payment was made.
+        """
+        wf_info = IWorkflowInfo(self.__parent__)
+        try:
+            wf_info.fireTransition('pay')
+        except InvalidTransitionError:
+            msg = log = 'Error: %s' % sys.exc_info()[1]
+            return False, msg, log
+        log = 'successful payment: %s' % self.p_id
+        msg = _('Successful payment')
+        return False, msg, log
+
+    def approveApplicantPayment(self):
+        """Approve payment and process applicant.
+        """
+        if self.p_state == 'paid':
+            return False, _('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'
+
+# 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/tags/0.2/src/waeup/kofa/applicants/pdf.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/pdf.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/pdf.py	(revision 10886)
@@ -0,0 +1,198 @@
+## $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')
+    note = None
+
+    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 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 = []
+        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))
+
+        # 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)
+
+        # 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))
+
+        # 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 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)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/permissions.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/permissions.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/permissions.py	(revision 10886)
@@ -0,0 +1,69 @@
+## $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):
+    grok.name('waeup.handleApplication')
+
+class ViewApplication(grok.Permission):
+    grok.name('waeup.viewApplication')
+
+class ViewApplicationsTab(grok.Permission):
+    grok.name('waeup.viewApplicantsTab')
+
+class ViewMyApplicationDataTab(grok.Permission):
+    grok.name('waeup.viewMyApplicationDataTab')
+
+class ManageApplication(grok.Permission):
+    grok.name('waeup.manageApplication')
+
+class ViewApplicationStatistics(grok.Permission):
+    grok.name('waeup.viewApplicationStatistics')
+
+class PayApplicant(grok.Permission):
+    grok.name('waeup.payApplicant')
+
+# Local role
+class ApplicationOwner(grok.Role):
+    grok.name('waeup.local.ApplicationOwner')
+    grok.title(u'Application Owner')
+    grok.permissions('waeup.handleApplication', 'waeup.viewApplication',
+                     'waeup.payApplicant')
+
+# Site role
+
+class ApplicantRole(grok.Role):
+    grok.name('waeup.Applicant')
+    grok.permissions('waeup.viewAcademics', 'waeup.viewMyApplicationDataTab',
+                     'waeup.Authenticated')
+
+class ApplicationsOfficer(grok.Role):
+    grok.name('waeup.ApplicationsOfficer')
+    grok.title(u'Applications Officer (view only)')
+    grok.permissions('waeup.viewApplication', 'waeup.viewApplicantsTab')
+
+class ApplicationsManager(grok.Role):
+    grok.name('waeup.ApplicationsManager')
+    grok.title(u'Applications Manager')
+    grok.permissions('waeup.manageApplication', 'waeup.viewApplication',
+                     'waeup.viewApplicantsTab', 'waeup.payApplicant')
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/root.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/root.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/root.py	(revision 10886)
@@ -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
+##
+"""
+The root for applicants.
+"""
+import grok
+from hurry.query import Eq
+from hurry.query.interfaces import IQuery
+from zope.component import getUtility
+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 = []
+
+    #: A dictionary to hold per language translations of description string.
+    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 should 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.
+        """
+        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)
+            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/tags/0.2/src/waeup/kofa/applicants/tests/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/__init__.py	(revision 10886)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/sample_applicant_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/sample_applicant_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/sample_applicant_data.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/tests/sample_applicant_data_update.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/sample_applicant_data_update.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/sample_applicant_data_update.csv	(revision 10886)
@@ -0,0 +1,4 @@
+applicant_id,reg_number,firstname,middlename,state
+,1001,Aaren,,
+dp2011_2345,6666,Alfons,,admitted
+,1003,Abraham,,
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/sample_applicant_data_update2.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/sample_applicant_data_update2.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/sample_applicant_data_update2.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/tests/sample_container_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/sample_container_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/sample_container_data.csv	(revision 10886)
@@ -0,0 +1,7 @@
+code,title,prefix,year,application_category,description,startdate,enddate,strict_deadline,mode
+app2012,General Studies 2012/2013,app,2012,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
+app2013,General Studies 2013/2014,app,2012,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/tags/0.2/src/waeup/kofa/applicants/tests/sample_faulty_applicant_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/sample_faulty_applicant_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/sample_faulty_applicant_data.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/tests/test_applicant.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_applicant.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_applicant.py	(revision 10886)
@@ -0,0 +1,216 @@
+## $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.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
+
+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/tags/0.2/src/waeup/kofa/applicants/tests/test_applicantcopier.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_applicantcopier.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_applicantcopier.py	(revision 10886)
@@ -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
+##
+"""
+Tests for the creation of student containers with data from admitted applicants.
+"""
+import os
+import grok
+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 waeup.kofa.testing import FunctionalLayer
+from waeup.kofa.interfaces import IExtFileStore, IFileStoreNameChooser
+from waeup.kofa.applicants.tests.test_browser import ApplicantsFullSetup
+
+session = datetime.now().year - 2
+
+class ApplicantCopierFunctionalTests(ApplicantsFullSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantCopierFunctionalTests, self).setUp()
+        return
+
+    def prepare_applicant(self):
+        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')
+        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.prepare_applicant()
+        storage = getUtility(IExtFileStore)
+        file_id = IFileStoreNameChooser(self.applicant).chooseName()
+        # The stored image can be fetched
+        fd = storage.getFile(file_id)
+        file_len_orig = len(fd.read())
+        # 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()
+        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)
+        # Ok, lets add the certificate and try again
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate)
+        (success, msg) = self.applicant.createStudent()
+        self.assertTrue('created' in msg)
+        # 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
+        storage = getUtility(IExtFileStore)
+        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 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')
+        # 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.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.prepare_applicant()
+        self.browser.open(self.container_path + '/createallstudents')
+        self.assertTrue('No student could be created' in self.browser.contents)
+        IWorkflowState(self.applicant).setState('admitted')
+        notify(grok.ObjectModifiedEvent(self.applicant))
+        self.browser.open(self.container_path + '/createallstudents')
+        self.assertTrue('No student could be created' in self.browser.contents)
+        logcontent = open(logfile).read()
+        self.assertTrue('No course admitted provided' in logcontent)
+        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('No student could be created' in self.browser.contents)
+        logcontent = open(logfile).read()
+        self.assertTrue('RequiredMissing: date_of_birth' in logcontent)
+        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.assertTrue('1 students successfully created' in self.browser.contents)
+
+    def test_hidden_batch_copying_all(self):
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'applicants.log')
+        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 student could be created' in self.browser.contents)
+        logcontent = open(logfile).read()
+        self.assertTrue('No course admitted provided' in logcontent)
+        self.applicant.course_admitted = self.certificate
+        self.browser.open(self.root_path + '/createallstudents')
+        self.assertTrue('1 students successfully created' 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)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_authentication.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_authentication.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_authentication.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/tests/test_batching.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_batching.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_batching.py	(revision 10886)
@@ -0,0 +1,416 @@
+## $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 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)
+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
+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(',')
+
+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('app2012', None)
+        container2 = self.app['applicants'].get('app2013', None)
+        self.assertTrue(container is not None)
+        self.assertTrue(container2 is not None)
+
+        # check attributes
+        self.assertEqual(container.code, u'app2012')
+        self.assertEqual(container.title, u'General Studies 2012/2013')
+        self.assertEqual(container.prefix, u'app')
+        self.assertEqual(container.year, 2012)
+        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()
+        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)
+
+        self.logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'applicants.log')
+
+    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 - Applicant record removed' in
+            logcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_browser.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_browser.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_browser.py	(revision 10886)
@@ -0,0 +1,1281 @@
+## $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
+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.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.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.tests.test_async import FunctionalAsyncTestCase
+
+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
+
+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.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 applicant 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 application 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 application section' in self.browser.contents)
+        # There is a manage link
+        link = self.browser.getLink('Manage application 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 application 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)
+        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})
+        #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})
+        #self.assertEqual(self.applicantscontainer.statistics[1],
+        #    {u'fac1': 1})
+        return
+
+    def test_add_delete_applicants(self):
+        # 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)
+        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)
+        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
+
+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_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 application section' in self.browser.contents)
+        # ... and can't access the manage page
+        self.assertRaises(
+            Unauthorized, self.browser.open, self.manage_path)
+        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 * 21  # A string of 21 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_pay_portal_application_fee(self):
+        self.login()
+        self.browser.open(self.edit_path)
+        # Payment tickets can't be created before the form has been validated
+        self.browser.getControl("Add online payment ticket").click()
+        self.assertTrue('Required input is missing' in self.browser.contents)
+        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()
+        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
+        configuration.application_fee = 200.0
+        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)
+        # 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()
+        # The payment slip can be downloaded now
+        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')
+        # 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 - payment 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)
+        return
+
+    def test_pay_container_application_fee(self):
+        self.login()
+        self.browser.open(self.edit_path)
+        self.fill_correct_values()
+        self.browser.getControl("Save").click()
+        configuration = SessionConfiguration()
+        configuration.academic_session = session_1
+        self.applicantscontainer.application_fee = 120.0
+        configuration.application_fee = 9999.9
+        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('...Payment ticket created...',
+                           self.browser.contents)
+        self.assertFalse(
+            '<span>9999.9</span>' in self.browser.contents)
+        self.assertTrue(
+            '<span>120.0</span>' in self.browser.contents)
+        self.assertTrue(
+            '<span>Application Fee</span>' in self.browser.contents)
+        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
+        configuration = SessionConfiguration()
+        configuration.academic_session = session_1
+        configuration.transcript_fee = 200.0
+        self.app['configuration'].addSessionConfiguration(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.sex").value = ['m']
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl("Save").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.browser.getLink("Logout").click()
+        # Login as manager
+        #self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        #self.browser.open(applicant_path + '/manage')
+        # When unselecting special_application, the payment ticket
+        # can still be created.
+        #self.browser.getControl(name="form.special_application").value = [
+        #    '']
+        #self.browser.getControl("Save").click()
+        #self.browser.getControl("Add online payment ticket").click()
+        #self.assertMatches('...Payment ticket created...',
+        #                   self.browser.contents)
+        #self.assertMatches('...<span>--</span>...',
+        #                   self.browser.contents)
+        return
+
+    def test_duplicate_choice(self):
+        # Make sure that that 1st and 2nd choice are different
+        self.login()
+        self.browser.open(self.edit_path)
+        self.fill_correct_values() # fill other fields with correct values
+        self.browser.getControl(name="form.course2").value = ['CERT1']
+        self.browser.getControl("Save").click()
+        self.assertTrue(
+            '1st and 2nd choice must be different' in self.browser.contents)
+        self.browser.getControl(name="form.course2").value = []
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved' in self.browser.contents)
+        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('Final Submit' in self.browser.contents)
+        IWorkflowInfo(self.applicant).fireTransition('pay')
+        self.browser.open(self.edit_path)
+        self.assertTrue('Final Submit' in self.browser.contents)
+        self.fill_correct_values() # fill other fields with correct values
+        self.browser.getControl("Save").click()
+        self.browser.getControl("Final Submit").click()
+        # We forgot to upload a passport picture
+        self.assertTrue(
+            'No passport picture uploaded' in self.browser.contents)
+        # 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("Final 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("Final 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("Final Submit").click()
+        self.assertTrue(
+            'Application submitted' in self.browser.contents)
+        self.browser.goBack(count=1)
+        self.browser.getControl("Save").click()
+        # The form is locked.
+        self.assertTrue(self.applicant.locked)
+        self.assertTrue(
+            'The requested form is locked' in self.browser.contents)
+        self.browser.goBack(count=1)
+        self.browser.getControl("Final Submit").click()
+        self.assertTrue(
+            'The requested form is locked' 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)
+
+class ApplicantRegisterTests(ApplicantsFullSetup):
+    # Tests for applicant registration
+
+    layer = FunctionalLayer
+
+    def test_register_applicant_create(self):
+        # An applicant can register himself.
+        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()
+        self.assertEqual(self.browser.url,
+            self.container_path + '/registration_complete?email=xx%40yy.zz')
+        # 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_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.firstname").value = 'John'
+        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 firstname attribute is not set.
+        self.applicantscontainer.mode = 'update'
+        self.browser.open(self.container_path + '/register')
+        self.browser.getControl(name="form.firstname").value = 'John'
+        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.firstname = u'John'
+        self.browser.open(self.container_path + '/register')
+        self.browser.getControl(name="form.firstname").value = 'Johnny'
+        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 firstname verification failed.
+        # It seems that the record doesn't exist.
+        self.assertTrue('No application record found.'
+            in self.browser.contents)
+        # Even with the correct firstname we can't register if a
+        # password has been set and used.
+        IWorkflowState(self.applicant).setState('started')
+        self.browser.getControl(name="form.firstname").value = 'John'
+        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 firstname field, used for verification, is not case-sensitive.
+        self.browser.getControl(name="form.firstname").value = 'jOhn'
+        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)
+
+class ApplicantsExportTests(ApplicantsFullSetup, 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 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 applicants").click()
+        self.browser.getControl("Start new export").click()
+
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        self.browser.open(container_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_applicants_%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(container_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 - applicants.browser.ExportJobContainerJobStart - '
+            'exported: applicants (%s), job_id=%s'
+            % (container_name_1, job_id) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - applicants.browser.ExportJobContainerDownload '
+            '- downloaded: WAeUP.Kofa_applicants_%s.csv, job_id=%s'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - applicants.browser.ExportJobContainerOverview '
+            '- discarded: job_id=%s' % job_id in logcontent
+            )
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_catalog.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_catalog.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_catalog.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/tests/test_container.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_container.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_container.py	(revision 10886)
@@ -0,0 +1,59 @@
+## $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 test_base(self):
+        # We cannot call the fundamental methods of a base in that case
+        container = ApplicantsContainer()
+        self.assertRaises(
+            NotImplementedError, container.archive)
+        self.assertRaises(
+            NotImplementedError, container.clear)
+
+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/tags/0.2/src/waeup/kofa/applicants/tests/test_dynamicroles.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_dynamicroles.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_dynamicroles.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/tests/test_export.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_export.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_export.py	(revision 10886)
@@ -0,0 +1,238 @@
+import datetime
+import os
+import pytz
+import shutil
+import tempfile
+import unittest
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility
+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, ApplicantsExporter)
+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'dp2012'
+        container.title = u'General Studies 2012/13'
+        container.prefix = list(ApplicationTypeSource()(container))[0]
+        container.year = 2012
+        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(
+            2012, 1, 1, 12, 0, 0, tzinfo=pytz.utc)
+        container.enddate = datetime.datetime(
+            2012, 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,code,description,'
+            'enddate,hidden,mode,prefix,startdate,strict_deadline,title,year\r\n'
+
+            'basic,0.0,dp2012,'
+            '"Some Description\nwith linebreak\n<<de>>man spriht deutsh",'
+            '2012-01-31 23:00:00+00:00,0,,app,2012-01-01 12:00:00+00:00,1,'
+            'General Studies 2012/13,2012\r\n'
+            )
+        return
+
+class ApplicantsExporterTest(ApplicantImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantsExporterTest, 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 = ApplicantsExporter()
+        verifyObject(ICSVExporter, obj)
+        verifyClass(ICSVExporter, ApplicantsExporter)
+        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 = ApplicantsExporter()
+        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,application_date,application_number,course1,course2,'
+            'course_admitted,date_of_birth,display_fullname,email,firstname,'
+            'history,lastname,locked,middlename,notice,password,phone,'
+            'reg_number,sex,special_application,state,'
+            'student_id,suspended,container_code\r\n'
+            'dp2011_654321,,654321,,,,,Anna Tester,,Anna,'
+            in result)
+        self.assertTrue(
+            'Application initialized by system\'],Tester,'
+            '0,,,,,,,,initialized,,0,dp2011\r\n'
+            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 = ApplicantsExporter()
+        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,application_date,application_number,course1,course2,'
+            'course_admitted,date_of_birth,display_fullname,email,firstname,'
+            'history,lastname,locked,middlename,notice,password,phone,'
+            'reg_number,sex,special_application,state,'
+            'student_id,suspended,container_code\r\n'
+            'dp2011_654321,,654321,CERT1,CERT1,CERT1,1981-02-04#,'
+            'Anna M. Tester,anna@sample.com,Anna,'
+            in result)
+        self.assertTrue(
+            'Application initialized by system\'],'
+            'Tester,0,M.,"Some notice\nin lines.",any password,'
+            '+234-123-12345#,123456,f,,initialized,,0,dp2011\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 = ApplicantsExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'applicant_id,application_date,application_number,course1,course2,'
+            'course_admitted,date_of_birth,display_fullname,email,firstname,'
+            'history,lastname,locked,middlename,notice,password,phone,'
+            'reg_number,sex,special_application,state,'
+            'student_id,suspended,container_code\r\n'
+            'dp2011_654321,,654321,CERT1,CERT1,CERT1,1981-02-04#,'
+            'Anna M. Tester,anna@sample.com,Anna,'
+            in result)
+        self.assertTrue(
+            'Application initialized by system\'],'
+            'Tester,0,M.,"Some notice\nin lines.",any password,'
+            '+234-123-12345#,123456,f,,initialized,,0,dp2011\r\n'
+            in result)
+        return
+
+    def test_export_filtered(self):
+        self.applicant = self.setup_applicant(self.applicant)
+        exporter = ApplicantsExporter()
+        exporter.export_filtered(
+            self.app, self.outfile, container=self.container.code)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'applicant_id,application_date,application_number,course1,course2,'
+            'course_admitted,date_of_birth,display_fullname,email,firstname,'
+            'history,lastname,locked,middlename,notice,password,phone,'
+            'reg_number,sex,special_application,state,'
+            'student_id,suspended,container_code\r\n'
+            'dp2011_654321,,654321,CERT1,CERT1,CERT1,1981-02-04#,'
+            'Anna M. Tester,anna@sample.com,Anna,'
+            in result)
+        self.assertTrue(
+            'Application initialized by system\'],'
+            'Tester,0,M.,"Some notice\nin lines.",any password,'
+            '+234-123-12345#,123456,f,,initialized,,0,dp2011\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,application_date,application_number,course1,'
+            'course2,course_admitted,date_of_birth,display_fullname,email,'
+            'firstname,history,lastname,locked,middlename,notice,password,'
+            'phone,reg_number,sex,special_application,state,student_id,'
+            'suspended,container_code\r\n'
+            in result)
+        return
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_interfaces.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_interfaces.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_interfaces.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/tests/test_payment.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_payment.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_payment.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/tests/test_pdf.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_pdf.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_pdf.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/tests/test_root.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_root.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_root.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/tests/test_webservices.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_webservices.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/tests/test_webservices.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/utils.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/utils.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/utils.py	(revision 10886)
@@ -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
+##
+"""General helper functions and utilities for the application 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)
+
+class ApplicantsUtils(grok.GlobalUtility):
+    """A collection of parameters and methods subject to customization.
+    """
+    grok.implements(IApplicantsUtils)
+
+    APP_TYPES_DICT = {
+      'app': ['General Studies', 'APP'],
+      'special': ['Special Application', 'SPE'],
+      }
+
+    SEPARATORS_DICT = {
+      'form.course1': _(u'Desired Study Courses'),
+      'form.student_id': _(u'Process Data'),
+      }
+
+    def setPaymentDetails(self, container, payment, applicant):
+        """Set the payment data of an applicant.
+        """
+        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 container.prefix.startswith('special'):
+            if applicant.special_application:
+                fee_name = applicant.special_application + '_fee'
+                payment.amount_auth = getattr(session_config, fee_name, 0.0)
+                payment.p_category = applicant.special_application
+            return
+        payment.p_category = 'application'
+        container_fee = container.application_fee
+        if container_fee:
+            payment.amount_auth = container_fee
+            return
+        payment.amount_auth = session_config.application_fee
+        if payment.amount_auth in (0.0, None):
+            return _('Amount could not be determined.'), None
+        return
+
+    def getApplicantsStatistics(self, container):
+        """Count applicants in containers.
+        """
+        state_stats = {INITIALIZED:0, STARTED:0, PAID:0, SUBMITTED:0,
+            ADMITTED:0, NOT_ADMITTED:0, CREATED:0}
+        cat = getUtility(ICatalog, name='applicants_catalog')
+        code = container.code
+        year = int(code[-4:])
+        target = code[:-4]
+        mxcode = target + str(year + 1)
+        for state in state_stats:
+            state_stats[state] = len(cat.searchResults(
+                state=(state, state),
+                applicant_id=(code, mxcode)))
+        return state_stats, None
+
+    def filterCertificates(self, context, resultset):
+        """Filter and sort certificates in AppCatCertificateSource.
+        """
+        resultlist = sorted(resultset, key=lambda value: value.code)
+        curr_course = context.course1
+        if curr_course is not None and curr_course not in resultlist:
+            # display also current course even if certificate has been removed
+            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/tags/0.2/src/waeup/kofa/applicants/viewlets.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/viewlets.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/viewlets.py	(revision 10886)
@@ -0,0 +1,264 @@
+## $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 application 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 ApplicantsContainerStatisticsActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsContainerManageFormPage)
+    grok.require('waeup.viewApplicationStatistics')
+    icon = 'actionicon_statistics.png'
+    text = _('Container statistics')
+    target = 'statistics'
+
+class ApplicantsContainerStatisticsActionButton2(
+        ApplicantsContainerStatisticsActionButton):
+    grok.order(3)
+    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 applicants' button for faculties.
+    """
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsContainerPage)
+    grok.require('waeup.manageApplication')
+    icon = 'actionicon_down.png'
+    text = _('Export applicants')
+    target = 'exports'
+    grok.order(4)
+
+class ApplicantsRootCreateStudentsActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(IApplicantsRoot)
+    grok.view(ApplicantsRootPage)
+    grok.require('waeup.managePortal')
+    icon = 'actionicon_entrance.png'
+    text = _('Create students')
+    target ='createallstudents'
+
+class ApplicantsContainerCreateStudentsActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsStatisticsPage)
+    grok.require('waeup.managePortal')
+    icon = 'actionicon_entrance.png'
+    text = _('Create students')
+    target ='createallstudents'
+
+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'):
+            return
+        return self.view.url(self.view.context, self.target)
+
+class StudentCreateActionButton(ManageActionButton):
+    grok.order(4)
+    grok.context(IApplicant)
+    grok.require('waeup.manageApplication')
+    icon = 'actionicon_entrance.png'
+    text = _('Create student record')
+    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 == 'paid':
+            return ''
+        return self.view.url(self.view.context, self.target)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/webservices.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/webservices.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/webservices.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/applicants/workflow.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/workflow.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/applicants/workflow.py	(revision 10886)
@@ -0,0 +1,202 @@
+## $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'
+
+IMPORTABLE_STATES = (INITIALIZED, STARTED, PAID, SUBMITTED, ADMITTED, NOT_ADMITTED)
+
+application_states_dict = {
+    INITIALIZED: _('initialized'),
+    STARTED: _('started'),
+    PAID: _('paid'),
+    SUBMITTED: _('submitted'),
+    ADMITTED: _('admitted'),
+    NOT_ADMITTED: _('not admitted'),
+    CREATED: _('created'),
+    }
+
+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 = 'refuse2',
+        title = _('Refuse application'),
+        msg = _('Application refused'),
+        source = ADMITTED,
+        destination = NOT_ADMITTED),
+
+    Transition(
+        transition_id = 'create',
+        title = _('Create student record'),
+        msg = _('Student record created'),
+        source = ADMITTED,
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'reset1',
+        title = _('Reset application to started'),
+        msg = _('Application reset'),
+        source = SUBMITTED,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset2',
+        title = _('Reset application to started'),
+        msg = _('Application reset'),
+        source = ADMITTED,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset3',
+        title = _('Reset application to started'),
+        msg = _('Application reset'),
+        source = NOT_ADMITTED,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset4',
+        title = _('Reset application to started'),
+        msg = _('Application reset'),
+        source = CREATED,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset5',
+        title = _('Reset application to paid'),
+        msg = _('Application reset to paid'),
+        source = SUBMITTED,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'reset6',
+        title = _('Reset application to started'),
+        msg = _('Application reset'),
+        source = PAID,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset7',
+        title = _('Reset application to admitted'),
+        msg = _('Application reset to admitted'),
+        source = CREATED,
+        destination = ADMITTED),
+    )
+
+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 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/tags/0.2/src/waeup/kofa/async.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/async.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/async.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/async.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/async.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/async.py	(revision 10886)
@@ -0,0 +1,277 @@
+## $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.hooks import getSite
+from zope.event import notify
+from waeup.kofa.interfaces import (
+    IJobManager, IProgressable, IJobContainer, WAEUP_KEY,
+    )
+
+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)
+        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
+        zope.component.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/tags/0.2/src/waeup/kofa/async_multidb.zcml
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/async_multidb.zcml	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/async_multidb.zcml	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/async_single.zcml
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/async_single.zcml	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/async_single.zcml	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/authentication.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/authentication.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/authentication.py	(revision 10886)
@@ -0,0 +1,551 @@
+## $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
+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.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
+
+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', '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):
+    grok.provides(ICredentialsPlugin)
+    grok.name('credentials')
+
+    loginpagename = 'login'
+    loginfield = 'form.login'
+    passwordfield = 'form.password'
+
+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.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):
+        if not isinstance(password, basestring):
+            return False
+        if not self.password:
+            # unset/empty passwords do never match
+            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) < 3:
+         errors.append('Password must have at least 3 chars.')
+       if pw != pw_repeat:
+         errors.append('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):
+    """A batch processor for IUserAccount objects.
+    """
+    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 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
+
+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
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/authentication.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/authentication.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/authentication.txt	(revision 10886)
@@ -0,0 +1,212 @@
+Kofa 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 users (principals)
+===========================
+
+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', 'bobsecret',
+  ...                           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.
+
+Users 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 = 'bobsecret'
+  >>> 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/tags/0.2/src/waeup/kofa/beaker.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/beaker.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/beaker.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/__init__.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/async.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/async.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/async.py	(revision 10886)
@@ -0,0 +1,151 @@
+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.browser.resources import jquery, loadbar
+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']
+        loadbar.need()
+
+    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):
+        jquery.need()
+        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/tags/0.2/src/waeup/kofa/browser/batchprocessing.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/batchprocessing.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/batchprocessing.txt	(revision 10886)
@@ -0,0 +1,654 @@
+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('Cancel').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()
+    >>> 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',
+    'Applicant Processor', 'Applicants Container Processor',
+    'CertificateCourse Processor', 'Certificate Processor',
+    'Course Processor',
+    'CourseTicket Processor',
+    'Department Processor', 'Faculty Processor',
+    'Hostel Processor',
+    'Student Payment 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()
+    >>> 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()
+    >>> 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()
+    >>> 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()
+    >>> 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']"
+    ... """)
+
+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()
+    >>> 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
+    ... """)
+
+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()
+    >>> 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()
+
+One line could not be imported:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC...
+    ...
+    ...Processing of 1 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. Skipping.
+
+    >>> 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()
+    >>> 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...
+    ...
+
+    >>> sorted(os.listdir(dc_path))
+    ['deleted', 'finished', 'logs', 'unfinished']
+
+    >>> os.listdir(dc_path + '/unfinished')
+    []
+
+    >>> sorted(os.listdir(dc_path + '/finished'))
+    ['certificates_zope.mgr.create.finished.csv', ...,
+    'users_zope.mgr.create.finished.csv', 'users_zope.mgr.csv']
+
+Processed (finished) Files
+==========================
+
+    >>> browser.open('http://localhost/app/datacenter/processed')
+    >>> '<a href="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/tags/0.2/src/waeup/kofa/browser/breadcrumbs.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/breadcrumbs.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/breadcrumbs.py	(revision 10886)
@@ -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'Portal Users')
+    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/tags/0.2/src/waeup/kofa/browser/breadcrumbs.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/breadcrumbs.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/breadcrumbs.txt	(revision 10886)
@@ -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'Portal Users']
+
+..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'Portal Users']
+
+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/tags/0.2/src/waeup/kofa/browser/browser.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/browser.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/browser.txt	(revision 10886)
@@ -0,0 +1,1357 @@
+Browsing Kofa
+*************
+
+Here we visit all 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 and see if the
+favicon is properly referenced::
+
+  >>> browser.open('http://localhost/myuniversity')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...href="http://localhost/myuniversity/@@/waeup_kofa/favicon.ico"
+  ...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
+
+The edit form contains the default value for the portal skin::
+
+  >>> 'waeup-base.css' 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')
+
+We can export a university as XML::
+
+  >>> browser.open('http://localhost/myuniversity/export.xml')
+  >>> print browser.contents
+  <?xml version="1.0" encoding="utf-8" ?>
+  <pickle>
+  ...
+  </pickle>
+
+  >>> print browser.headers
+  Status: 200 Ok
+  Content-Length: ...
+  Content-Type: text/xml; charset=UTF-8
+  X-Powered-By: Zope (www.zope.org), Python (www.python.org)
+
+
+Portal Users
+============
+
+  >>> browser.open('http://localhost/myuniversity')
+  >>> browser.getLink('Portal Users').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Portal Users...
+  ...
+
+We can add users:
+
+  >>> browser.getLink("Add user").click()
+  >>> browser.getControl(name="form.name").value = 'bob'
+  >>> browser.getControl(name="form.title").value = 'Bob The User'
+  >>> browser.getControl(name="password").value = 'secret'
+  >>> browser.getControl(name="control_password").value = 'secret'
+  >>> 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 user").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<td>bob</td>
+  ...
+
+We can edit user bob:
+
+  >>> 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>
+  ...
+
+Users 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 = 'secret'
+  >>> browser.getControl(name="control_password").value = 'secret'
+  >>> 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 user").click()
+  >>> 'The userid chosen already exists' in browser.contents
+  True
+
+Users 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="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="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 portal user page the new local role is displayed:
+
+  >>> browser.getLink("Portal Users").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="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="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 users 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'
+
+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
+
+  >>> 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("Portal Users").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 certificats.
+
+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 users 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'
+
+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
+
+
+CertificateCourses
+==================
+
+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 certificatecourses
+-------------------------
+
+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 certificatecourse:
+
+  >>> ctrl.getControl('COURSE1').selected = True
+  >>> browser.getControl(name='form.level').value = ['100']
+  >>> browser.getControl('Add certificate course').click()
+
+Our certificatecourse 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 certificatecourse 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 certificatecourse, 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 certificatecourses
+----------------------------
+
+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 and return").click()
+  >>> browser.getLink('COURSE1_100').click()
+  >>> browser.url
+  'http://localhost/myun.../TF/TD/certificates/CERT1/COURSE1_100'
+
+Let's set a new level (it was 100 before) and save the form. This will
+bring us to the certificate index page afterwards:
+
+  >>> browser.open(cert_url + '/COURSE1_100/manage')
+  >>> browser.getControl(name='form.level').value = ['200']
+  >>> browser.getControl("Save and return").click()
+
+As we changed the level, also the URL will change:
+
+  >>> browser.getLink('COURSE1_200').click()
+  >>> browser.url
+  'http://localhost/myun.../TF/TD/certificates/CERT1/COURSE1_200'
+
+If we go to the settings page and click ``Cancel`` nothing will be
+changed:
+
+  >>> browser.getLink('Edit certificate course').click()
+  >>> browser.getControl(name='form.level').value = ['400']
+  >>> browser.getControl('Cancel').click()
+
+Our certcourse provides a new level of 200 and not 400:
+
+  >>> browser.open(cert_url + '/COURSE1_200')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+  ...<td>Level:</td>
+  ...<td>200 (Year 2)</td>
+  ...
+
+Searching certificatecourses
+----------------------------
+
+  >>> 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 certificatecourses
+---------------------------
+
+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', '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', '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/tags/0.2/src/waeup/kofa/browser/captcha.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/captcha.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/captcha.py	(revision 10886)
@@ -0,0 +1,348 @@
+## $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 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 import resources
+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
+##
+API_SSL_SERVER = "https://www.google.com/recaptcha/api"
+VERIFY_SERVER = "https://www.google.com/recaptcha/api"
+
+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.
+
+    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'``.
+    """
+
+    grok.implements(ICaptcha)
+
+    #: name of solution field in HTTP request
+    sol_field = 'recaptcha_response_field'
+    #: name of challenge field in HTTP request
+    chal_field = 'recaptcha_challenge_field'
+    #: resource to set theme and other options
+    theme = resources.recaptcha_white
+
+    # 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"
+
+    def verify(self, request):
+        """Grab challenge/solution from HTTP request and verify it.
+
+        Verification happens against recaptcha remote API servers. It
+        only happens, when really a solution was sent with the
+        request.
+
+        Returns a :class:`CaptchaResponse` indicating that the
+        verification failed or succeeded.
+        """
+        form = getattr(request, 'form', {})
+        solution=form.get(self.sol_field, None)
+        challenge=form.get(self.chal_field, None)
+        if not challenge or not solution:
+            # Might be first-time display of the captcha: not a valid
+            # solution but no error code to prevent any error
+            # messages. Skip further verification.
+            return CaptchaResponse(
+                is_valid=False)
+        params = urllib.urlencode(
+            {
+                'privatekey': self.PRIVATE_KEY,
+                'remoteip': '127.0.0.1',
+                'challenge': challenge,
+                'response': solution,
+                })
+        request = urllib2.Request(
+            url = "%s/verify" % VERIFY_SERVER,
+            data = params,
+            headers = {
+                "Content-type": "application/x-www-form-urlencoded",
+                "User-agent": "reCAPTCHA Python Kofa",
+                }
+            )
+        resp = urllib2.urlopen(request)
+        ret_vals = resp.read().splitlines()
+        resp.close()
+        ret_code, err_code = ret_vals
+
+        if ret_code == "true":
+            return CaptchaResponse(is_valid=True)
+        return CaptchaResponse(is_valid=False, error_code=ret_vals[1])
+
+    def display(self, error_code=None):
+        """Display challenge and input field for solution as HTML.
+
+        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).
+        """
+        if self.theme is not None:
+            self.theme.need()
+        error_param = ''
+        if error_code:
+            error_param = '&error=%s' % error_code
+
+        html = (
+            u'<script type="text/javascript" '
+            u'src="%(ApiServer)s/challenge?k=%(PublicKey)s%(ErrorParam)s">'
+            u'</script>'
+            u''
+            u'<noscript>'
+            u'<iframe'
+            u'    src="%(ApiServer)s/noscript?k=%(PublicKey)s%(ErrorParam)s"'
+            u'    height="300" width="500" frameborder="0"></iframe><br />'
+            u'<textarea name="recaptcha_challenge_field"'
+            u'          rows="3" cols="40"></textarea>'
+            u'<input type="hidden" name="recaptcha_response_field"'
+            u'       value="manual_challenge" />'
+            u'</noscript>' % {
+                'ApiServer' : API_SSL_SERVER,
+                'PublicKey' : self.PUBLIC_KEY,
+                'ErrorParam' : error_param,
+                }
+            )
+        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/tags/0.2/src/waeup/kofa/browser/exceptions.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/exceptions.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/exceptions.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/interfaces.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/interfaces.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/interfaces.py	(revision 10886)
@@ -0,0 +1,236 @@
+## $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 ITheme(Interface):
+    """An theme or 'skin'.
+
+    Themes are basically collections of CSS- and/or JavaScript files
+    stored somewhere. In Grok-contexts these files can be registered
+    as 'resources' (see :mod:`waeup.kofa.browser.resources`).
+
+    Furthermore, to make themes selectable and distinctable from each
+    other we assign them short descriptions.
+
+    Themes should be registered as named utilities so that they can be
+    defined also in third-party packages and be looked up by other
+    components like theme-selectors.
+    """
+    description = schema.TextLine(
+        title = u'Description',
+        description = u'Short description of the theme for choice fields',
+        required = False,
+        )
+
+    def getResources():
+        """Get resources of the theme.
+
+        Deliver a list of resources that must be included in a web page to
+        let it be rendered in the theme's style.
+
+        A resource is normally defined in the `resources` module. If a
+        theme needs several of them which normally do not depend on
+        each other then all these resources can be returned by this
+        method. Normally, you will only return one resource.
+
+        These resources will (if the theme is selected) be `need()`-ed
+        by the `update()` method of the general site layout.
+        """
+
+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 (whatever it is) should
+       implement this interface.
+    """
+    student = Attribute('''Some student object that has a '''
+                        ''' `display_fullname` attribute.''')
+
+class IApplicantBase(IKofaObject):
+    """Some Applicant.
+    """
+    display_fullname = Attribute('''Fullname.''')
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/layout.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/layout.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/layout.py	(revision 10886)
@@ -0,0 +1,345 @@
+## $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 hurry.resource import mode
+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 (
+    ITheme, IStudentNavigationBase, IApplicantBase)
+from waeup.kofa.browser.theming import get_all_themes, KofaThemeBase
+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='', **options):
+        super(KofaAction, self).__init__(label, **options)
+        self.style = style
+
+    def render(self):
+        if not self.available():
+            return ''
+        label = self.label
+        if isinstance(label, Message):
+            label = translate(self.label, context=self.form.request)
+        return ('<input type="submit" id="%s" name="%s" value="%s"'
+                ' class="btn %s"/>' %
+                (self.__name__, self.__name__, escape(label, quote=True),
+                self.style))
+
+class jsaction(grok.action):
+
+    def __call__(self, success):
+        action = JSAction(self.label, success=success, **self.options)
+        self.actions.append(action)
+        return action
+
+class JSAction(Action):
+
+    msg = _('\'Are you sure?\'')
+
+    def render(self):
+        if not self.available():
+            return ''
+        label = self.label
+        if isinstance(label, Message):
+            label = translate(self.label, context=self.form.request)
+        msg = translate(self.msg, context=self.form.request)
+        return ('<input type="submit" id="%s" name="%s" value="%s"'
+                ' class="btn" onclick="return confirmPost(%s)" />' %
+                (self.__name__, self.__name__, escape(label, quote=True), msg)
+                )
+
+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='alert-message warning'):
+        """Send a short message to the user.
+        """
+        source = queryUtility(IMessageSource, name='session')
+        if source is None:
+            return None
+        source.send(message, type)
+        return True
+
+class KofaLayout(UtilityView,Layout):
+    """A megrok.layout.Layout with additional methods.
+    """
+    grok.baseclass()
+
+class KofaForm(UtilityView,Form):
+    """A megrok.layout.Form with additional methods.
+    """
+    grok.baseclass()
+
+    def setUpWidgets(self,ignore_request=False):
+        super(KofaForm,self).setUpWidgets(ignore_request)
+        # Width parameters will be overridden by Bootstrap
+        # so we have to set the css class
+        if self.widgets.get('subject'):
+            self.widgets['subject'].cssClass = 'span9'
+        if self.widgets.get('body'):
+            self.widgets['body'].height = 10
+        if self.widgets.get('body'):
+            self.widgets['body'].cssClass = 'span9'
+
+class KofaPage(UtilityView,Page):
+    """A megrok.layout page with additional methods.
+    """
+    grok.baseclass()
+
+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 widget.__class__.__name__ == 'TextWidget':
+                widget.cssClass = 'span8'
+            elif widget.__class__.__name__ == 'IntWidget':
+                widget.cssClass = 'span2'
+            if 'address' in widget.name or \
+                'comment' in widget.name or \
+                'notice' in widget.name:
+                widget.cssClass = 'span8'
+                widget.height = 6
+        if self.widgets.get('title'):
+            self.widgets['title'].cssClass = 'span12'
+        if self.widgets.get('frontpage'):
+            self.widgets['frontpage'].cssClass = 'span12'
+        if self.widgets.get('phone'):
+            self.widgets['phone'].cssClass = 'span4'
+        if self.widgets.get('transcript_comment'):
+            self.widgets['transcript_comment'].cssClass = 'span12'
+            self.widgets['transcript_comment'].height = 12
+        if self.widgets.get('jamb_subjects'):
+            self.widgets['jamb_subjects'].cssClass = 'span8'
+            self.widgets['jamb_subjects'].height = 6
+        if self.widgets.get('description'):
+            self.widgets['description'].cssClass = 'span12'
+
+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
+    default_theme = KofaThemeBase()
+    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 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):
+            tz = getUtility(IKofaUtils).tzinfo
+            date = to_timezone(
+                datetimeobj, tz).strftime("%d/%m/%Y")
+            return date
+        else:
+            return None          
+
+    def update(self):
+        """Include the resources required by the chosen skin/theme.
+
+        University instances provide a 'skin' attribute, that should
+        hold the internal name of a theme.
+
+        A theme in the waeup.kofa sense consists of a list of
+        CSS/JavaScript resources defined in the
+        :mod:`waeup.kofa.browser.resources` module.
+
+        If the context University object has no such attribute or the
+        set value is not a valid theme name, we pick 'gray waeup
+        theme' as default.
+        """
+        mode('minified')
+        theme_name = getattr(grok.getSite()['configuration'], 'skin', '')
+        theme = queryUtility(ITheme, name=theme_name,
+                             default=self.default_theme)
+        for resource in theme.getResources():
+            resource.need()
+        return
+
+    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/tags/0.2/src/waeup/kofa/browser/pages.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/pages.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/pages.py	(revision 10886)
@@ -0,0 +1,2448 @@
+## $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 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.browser.layout import (
+    KofaPage, KofaForm, 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.browser.resources import (
+    warning, tabs, datatable)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.interfaces import(
+    IKofaObject, IUsersContainer, IUserAccount, IDataCenter,
+    IKofaXMLImporter, IKofaXMLExporter, IBatchProcessor,
+    ILocalRolesAssignable, DuplicationError, IConfigurationContainer,
+    ISessionConfiguration, ISessionConfigurationAdd, IJobManager,
+    IPasswordValidator, IContactForm, IKofaUtils, ICSVExporter,
+    academic_sessions_vocab)
+from waeup.kofa.permissions import (
+    get_users_with_local_roles, get_all_roles, get_all_users,
+    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.widgets.htmlwidget import HTMLDisplayWidget
+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.students.export import EXPORTER_NAMES as STUDENT_EXPORTERS
+from waeup.kofa.students.catalog import StudentQueryResultItem
+
+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.')
+        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.'))
+        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]))
+    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.'))
+        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]))
+    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):
+    """Doll up export jobs for displaying in table.
+    """
+    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')
+        args = ', '.join(['%s=%s' % (item[0], item[1])
+            for item in job.kwargs.items()])
+        status = job.finished and 'ready' or 'running'
+        status = job.failed and 'FAILED' or status
+        start_time = getattr(job, 'begin_after', None)
+        if start_time:
+            start_time = start_time.astimezone(
+                getUtility(
+                    IKofaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
+        download_url = view.url(view.context, 'download_export',
+                                data=dict(job_id=job_id))
+        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 = (job.finished and not job.failed),
+            show_refresh_button = not job.finished,
+            show_discard_button = job.finished,)
+        entries.append(new_entry)
+    return entries
+
+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 get_all_users()
+
+    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):
+        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.personal_data_expired:
+                        rel_link = '/students/%s/edit_personal' % (
+                            self.request.principal.id)
+                        self.flash(
+                          _('Your personal data record is outdated. Please update.'))
+                    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
+                if not self.camefrom:
+                    # User might have entered the URL directly. Let's beam
+                    # him back to our context.
+                    self.redirect(self.url(self.context))
+                    return
+                self.redirect(self.camefrom)
+                return
+            # Display appropriate flash message if credentials are correct
+            # but student has been deactivated or a temporary 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.
+                    delta = timedelta(minutes=10)
+                    now = datetime.utcnow()
+                    temp_password_dict = getattr(student, 'temp_password', None)
+                    if temp_password_dict is not None and \
+                        now < temp_password_dict.get('timestamp', now) + delta:
+                        self.flash(
+                            _('Your account has been temporarily deactivated.'))
+                        return
+                    # Now we know that the student is suspended.
+                    comment = self._comment(student)
+                    if comment:
+                        self.flash(comment)
+                    else:
+                        self.flash(_('Your account has been deactivated.'))
+                    return
+            self.flash(_('You entered invalid credentials.'))
+            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())
+
+
+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 ContactAdminForm(KofaForm):
+    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(ContactAdminForm, 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 EnquiriesForm(ContactAdminForm):
+    """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):
+        super(EnquiriesForm, 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.'))
+        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 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 = _('Portal Users')
+    manage_button = _(u'Manage')
+    delete_button = _(u'Remove')
+
+    def update(self, userid=None, adduser=None, manage=None, delete=None):
+        datatable.need()
+        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)
+    label = _('Add user')
+
+    @action(_('Add user'), 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_password(password, password_ctl)
+            if errors:
+                self.flash( ' '.join(errors))
+                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.')
+            return
+        self.redirect(self.url(self.context))
+
+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 user ${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_password(password, password_ctl)
+            if errors:
+                self.flash( ' '.join(errors))
+                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 ContactUserForm(ContactAdminForm):
+    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')
+    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
+
+#
+# Search pages...
+#
+
+class SearchPage(KofaPage):
+    """General search page for the academics 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):
+        datatable.need()
+        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)
+        return
+
+#
+# Configuration pages...
+#
+
+class ConfigurationContainerDisplayFormPage(KofaDisplayFormPage):
+    """View page of the configuration container.
+    """
+    grok.require('waeup.managePortalConfiguration')
+    grok.name('view')
+    grok.context(IConfigurationContainer)
+    pnav = 0
+    label = _(u'View portal configuration')
+    form_fields = grok.AutoFields(IConfigurationContainer)
+    form_fields['frontpage'].custom_widget = HTMLDisplayWidget
+
+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')]
+    tabtwoactions = [
+        _('Add session configuration'),
+        _('Remove selected')]
+    form_fields = grok.AutoFields(IConfigurationContainer).omit('frontpage_dict')
+
+    def update(self):
+        tabs.need()
+        self.tab1 = self.tab2 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        datatable.need()
+        warning.need()
+        return super(ConfigurationContainerManageFormPage, self).update()
+
+    def _frontpage(self):
+        view = ConfigurationContainerDisplayFormPage(
+            self.context,self.request)
+        view.setUpWidgets()
+        return view.widgets['frontpage']()
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        self.context.frontpage_dict = self._frontpage()
+        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'), validator=NullValidator)
+    def updatePlugins(self, **data):
+        grok.getSite().updatePlugins()
+        self.flash(_('Plugins were updated. See log file for details.'))
+        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.'))
+            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)
+        self.redirect(self.url(self.context.__parent__, '@@index')+'?tab2')
+        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
+
+    def update(self):
+        datatable.need()
+        warning.need()
+        return super(DatacenterPage, self).update()
+
+    @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.'))
+            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.'))
+                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
+
+    def update(self):
+        datatable.need()
+        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'Cancel')
+
+    def getPreviewHeader(self):
+        """Get the header fields of uploaded CSV file.
+        """
+        reader = csv.reader(open(self.fullpath, 'rb'))
+        return reader.next()
+
+    def getImporters(self):
+        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
+        importers = sorted(
+            [dict(title=x.name, name=x.util_name) for x in importers])
+        return importers
+
+    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.'))
+            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.'))
+            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."))
+            #    return
+            if not filename.endswith('.csv'):
+                self.flash(_("Only csv files are allowed."))
+                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."))
+                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:
+                self.flash(_(
+                    "Your file contains forbidden characters or "
+                    "has invalid CSV format. "
+                    "First problematic line detected: line %s. "
+                    "Please replace." % invalid_line))
+                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.')
+            self.flash('I/O error: %s' % sys.exc_info()[1])
+            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):
+        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
+        importer_props = []
+        for x in importers:
+            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 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 SkeletonDownloadView(UtilityView, grok.View):
+    grok.context(IDataCenter)
+    grok.name('skeleton')
+    grok.require('waeup.manageDataCenter')
+
+    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 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'Cancel')
+
+    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 cancel is not None:
+            self.flash(_('Import aborted.'))
+            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'))
+
+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')
+
+    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.'))
+            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!'))
+                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)
+
+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')
+
+    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):
+        datatable.need()
+        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.'))
+            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)
+
+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')
+
+    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
+        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)
+
+        if self.warn_num:
+            self.flash(_('Processing of ${a} rows failed.',
+                mapping = {'a':self.warn_num}))
+        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')
+
+    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')
+    placeholder = _('Enter a regular expression here...')
+
+    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.'))
+            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 search is None or not query:
+            return
+        try:
+            self.result = ''.join(
+                self.context.queryLogfiles(logname, query))
+        except ValueError:
+            self.flash(_('Invalid search expression.'))
+            return
+        if not self.result:
+            self.flash(_('No search results found.'))
+        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'Cancel')
+
+    def update(self, newpath=None, move=False, overwrite=False,
+               save=None, cancel=None):
+        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}))
+        except:
+            self.flash(_('Given storage path cannot be used.'))
+            self.flash(_('Error: ${a}', mapping = {'a':sys.exc_info()[1]}))
+            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')
+
+    def getExporters(self):
+        utils = getUtilitiesFor(ICSVExporter)
+        title_name_tuples = [
+            (util.title, name) for name, util in utils
+            if not name in STUDENT_EXPORTERS]
+        # The exporter for access codes requires a special permission.
+        if not checkPermission('waeup.manageACBatches', self.context):
+            title_name_tuples.remove((u'AccessCodes', u'accesscodes'))
+        return sorted(title_name_tuples)
+
+    def update(self, CREATE=None, DISCARD=None, exporter=None, job_id=None):
+        if CREATE:
+            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)
+        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 ExportXMLPage(grok.View):
+    """Deliver an XML representation of the context.
+    """
+    grok.name('export.xml')
+    grok.require('waeup.managePortal')
+
+    def render(self):
+        exporter = IKofaXMLExporter(self.context)
+        xml = exporter.export().read()
+        self.response.setHeader(
+            'Content-Type', 'text/xml; charset=UTF-8')
+        return xml
+
+class ImportXMLPage(KofaPage):
+    """Replace the context object by an object created from an XML
+       representation.
+
+       XXX: This does not work for ISite objects, i.e. for instance
+            for complete University objects.
+    """
+    grok.name('importxml')
+    grok.require('waeup.managePortal')
+
+    def update(self, xmlfile=None, CANCEL=None, SUBMIT=None):
+        if CANCEL is not None:
+            self.redirect(self.url(self.context))
+            return
+        if not xmlfile:
+            return
+        importer = IKofaXMLImporter(self.context)
+        obj = importer.doImport(xmlfile)
+        if type(obj) != type(self.context):
+            return
+        parent = self.context.__parent__
+        name = self.context.__name__
+        self.context = obj
+        if hasattr(parent, name):
+            setattr(parent, name, obj)
+        else:
+            del parent[name]
+            parent[name] = obj
+            pass
+        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')
+
+    @property
+    def label(self):
+        return _('Manage academic section')
+
+    def update(self):
+        warning.need()
+        return super(FacultiesContainerManageFormPage, self).update()
+
+    @jsaction(_('Remove selected'))
+    def delFaculties(self, **data):
+        if not checkPermission('waeup.managePortal', self.context):
+            self.flash(_('You are not allowed to remove entire faculties.'))
+            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
+
+    @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.'))
+            return
+        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')]
+
+    form_fields = grok.AutoFields(IFaculty).omit('code')
+
+    @property
+    def label(self):
+        return _('Manage faculty')
+
+    def update(self):
+        tabs.need()
+        self.tab1 = self.tab2 = self.tab3 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        warning.need()
+        datatable.need()
+        return super(FacultyManageFormPage, self).update()
+
+    @jsaction(_('Remove selected'))
+    def delDepartments(self, **data):
+        if not checkPermission('waeup.managePortal', self.context):
+            self.flash(_('You are not allowed to remove entire departments.'))
+            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):
+        datatable.need()
+        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'))
+            return
+        self.hitlist = self._find_students(query=self.searchterm,
+            searchtype=self.searchtype, view=self)
+        if not self.hitlist:
+            self.flash(_('No student found.'))
+        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
+
+    @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.'))
+            return
+        self.status = 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' % (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
+
+#
+# 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):
+        tabs.need()
+        datatable.need()
+        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')]
+
+    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))
+
+    def update(self):
+        tabs.need()
+        self.tab1 = self.tab2 = self.tab3 = self.tab4 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        warning.need()
+        datatable.need()
+        super(DepartmentManageFormPage, self).update()
+        return
+
+    @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.'))
+            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
+
+    @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)
+            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
+
+    @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)
+            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)
+
+    @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')]
+
+    form_fields = grok.AutoFields(ICourse).omit('code')
+
+    def update(self):
+        tabs.need()
+        self.tab1 = self.tab2 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        warning.need()
+        datatable.need()
+        return super(CourseManageFormPage, self).update()
+
+    @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)
+
+#
+# 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):
+        datatable.need()
+        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')
+
+    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')
+
+    def update(self):
+        tabs.need()
+        self.tab1 = self.tab2 = self.tab3 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        warning.need()
+        datatable.need()
+        return super(CertificateManageFormPage, self).update()
+
+    @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')
+
+    @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.'))
+            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
+
+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
+
+    @action(_('Save and return'), style='primary')
+    def saveAndReturn(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
+            parent.addCertCourse(**data)
+            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))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+class ChangePasswordRequestPage(KofaForm):
+    """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')
+    label = _('Send me a new password')
+    form_fields = grok.AutoFields(IChangePassword)
+
+    def update(self):
+        # 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 _searchUser(self, identifier, email):
+        # Search student
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(
+            #reg_number=(identifier, identifier),
+            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(
+                #reg_number=(identifier, identifier),
+                email=(email,email))
+            for result in results:
+                if result.applicant_id == identifier or \
+                    result.reg_number == identifier:
+                    return result
+        # Search portal user
+        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.'))
+            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.'))
+        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/tags/0.2/src/waeup/kofa/browser/pdf.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/pdf.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/pdf.py	(revision 10886)
@@ -0,0 +1,869 @@
+## $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, 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=10,
+    )
+
+#: 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)
+    If not html code:
+    - just replace newlines by <br> tags
+    """
+    if '</' in html:
+        # Add br tag if widgets contain div tags
+        # which are not supported by reportlab
+        html = html.replace('</div>', '</div><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.6 * 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)
+            #tz = getUtility(IKofaUtils).tzinfo
+            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()
+        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):
+        """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
+        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
+        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)
+
+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/tags/0.2/src/waeup/kofa/browser/reports.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/reports.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/reports.py	(revision 10886)
@@ -0,0 +1,129 @@
+## $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 zope.component import getUtility, queryUtility
+from zope.location.location import located
+from waeup.kofa.interfaces import IJobManager, IKofaUtils
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.browser.layout import KofaPage
+from waeup.kofa.reports import IReportsContainer, IReportGenerator
+from waeup.kofa.reports import get_generators
+
+grok.templatedir('templates')
+
+class ReportsContainerPage(KofaPage):
+    """A view on a reports container.
+    """
+    grok.name('index')
+    grok.context(IReportsContainer)
+    grok.require('waeup.manageReports')
+    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):
+        self.entries = []
+        if job_id and DISCARD:
+            entry = self.context.report_entry_from_job_id(job_id)
+            self.context.delete_report_entry(entry)
+            self.flash('Report discarded: %s' % job_id)
+            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+            grok.getSite().logger.info(
+                '%s - report %s discarded' % (ob_class, job_id))
+        self.entries = self._generate_entries(user_id=None)
+        if job_id and DOWNLOAD:
+            self.redirect(self._report_url(job_id))
+            return
+        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 = getattr(generator, 'title', 'Unknown')
+            args = ', '.join([str(x) for x in job.kwargs['kw'].values()])
+            descr = '%s (%s)' % (gen_title, args)
+            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.manageReports')
+    label = _('Create report')
+
+    def update(self, START_GENERATOR=None, generator=None):
+        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.
+        """
+        result = [(gen.title, name) for name, gen in get_generators()]
+        return result
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/resources.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/resources.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/resources.py	(revision 10886)
@@ -0,0 +1,314 @@
+## $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 JavaScript and CSS resources provided by waeup.kofa.
+"""
+#from hurry import yui
+from hurry.resource import Library, ResourceInclusion
+from hurry.resource.core import inclusion_renderers, EXTENSIONS
+from hurry.jqueryui import jqueryui, base, blitzer, ui_lightness, humanity
+from hurry.jquery import jquery
+
+
+#: All local resources are registered under the name ``waeup_kofa``.
+waeup_kofa = Library('waeup_kofa', 'static')
+
+def render_ico(url):
+    """An inclusion renderer for hurry.resource for files with .ico extension.
+    """
+    return '<link rel="shortcut icon" type="image/x-icon" href="%s" />' % url
+
+# Monkey patch hurry.resource to support .ico inclusions.
+inclusion_renderers['.ico'] = render_ico
+EXTENSIONS.append('.ico')
+
+#: A resource that includes a favicon.
+#:
+#: In the `getResource()` method of any theme include the
+#: HTML code to be rendered into the page::
+#:
+#:   from waeup.kofa.browser.resources import favicon
+#:   # ...
+#:   class MyTheme(grok.GlobalUtility):
+#:     # ...
+#:     def getResources(self):
+#:       return [favicon, ...]
+#:
+favicon = ResourceInclusion(waeup_kofa, 'favicon.ico')
+
+
+#: A resource that binds a jQueryUI datepicker widget to each
+#: <div> or <input> tag with class ``datepicker``.
+#: If you want to let a JavaScript datepicker appear when
+#: clicking into some input field, then you have to perform
+#: two steps:
+#:
+#: * In the `update()` method of the responsible view/page/form
+#:   require the JavaScript code to be rendered into the page::
+#:
+#:     from waeup.kofa.browser.resources import datepicker
+#:     # ...
+#:     class MyPage(...):
+#:       # ...
+#:       def update(self):
+#:         datepicker.need()
+#:
+#:   This way all required JavaScript will be rendered correctly
+#:   into the HTML page generated.
+#:
+#: * In your HTML code add some ``<input>`` or ``<div>`` tag with
+#:   ``class`` set to ``datepicker``, like this:
+#:
+#:   .. code-block:: html
+#:
+#:     <input type="text" name="entrydate" class="datepicker" />
+#:
+#: When the HTML is rendered, clicking into the field will open
+#: a datepicker widget. The datepicker functionality will be
+#: delivered by jQueryUI for every ``<div>/<input>`` field with class
+#: ``datepicker`` set.
+#:
+#: That means, you can also have several date input fields on a
+#: single page as long as each one has class ``datepicker``.
+#:
+#: Furthermore :func:`datepicker` supports different kinds of date
+#: representation from which you can choose simply by setting a
+#: different CSS class. The ``datepicker`` CSS mentioned above is
+#: only one of the following possibilities:
+#:
+#: ``"datepicker"``
+#:    renders date in format ``YYYY-MM-DD`` (ISO standard)
+#:
+#: ``"datepicker-year"``
+#:    same but also adds a select field for year
+#:
+#: ``"datepicker-le"``
+#:    renders date in format ``DD/MM/YYYY`` (little endian)
+#:
+#: ``"datepicker-le-year"``
+#:    same but also adds a select field for year
+#:
+#: ``"datepicker-de"``
+#:    renders date in format ``DD.MM.YYYY`` (german format)
+#:
+#: ``"datepicker-de-year"``
+#:    same but also adds a select field for year
+#:
+#: ``"datepicker-us"``
+#:    renders date in format ``MM/DD/YYYY`` (U.S. format)
+#:
+#: ``"datepicker-us-year"``
+#:    same but also adds a select field for year
+datepicker = ResourceInclusion(waeup_kofa, 'datepicker.js',
+                               depends=[jqueryui])
+
+
+#: If you want generate alert or confirm messages then you have to perform
+#: two steps:
+#:
+#: * In the `update()` method of the responsible view/page/form
+#:   require the JavaScript code to be rendered into the page::
+#:
+#:     from waeup.kofa.browser.resources import warning
+#:     # ...
+#:     class MyPage(...):
+#:       # ...
+#:       def update(self):
+#:         warning.need()
+#:
+#:   This way all required JavaScripts will be rendered correctly
+#:   into the HTML page generated.
+#:
+#:   In your HTML code add some <input> or <a> tag with onClick event:
+#:
+#:   .. code-block:: html
+#:
+#:        <input type="submit" name="xyz" value="abc">
+#:               onclick="return confirmPost('Are you sure?')"/>
+warning = ResourceInclusion(waeup_kofa, 'warning.js')
+
+#: If you have many select boxes in a from which have to be selected at the same
+#: time you have to perform two steps:
+#:
+#: * In the `update()` method of the responsible view/page/form
+#:   require the JavaScript code to be rendered into the page::
+#:
+#:     from waeup.kofa.browser.resources import toggleall
+#:     # ...
+#:     class MyPage(...):
+#:       # ...
+#:       def update(self):
+#:         toggleall.need()
+#:
+#:   This way all required JavaScripts will be rendered correctly
+#:   into the HTML page generated.
+#:
+#:   In your HTML code add some <input> tag with onClick event:
+#:
+#:   .. code-block:: html
+#:
+#:    <input type="checkbox" onClick="toggle(this, 'entries')" />
+toggleall = ResourceInclusion(waeup_kofa, 'toggleall.js')
+
+#: A resource that binds Bootstrap tabs to <div> tags with class
+#: ``tabs``.
+#:
+#: If you want Bootstrap tabs in your page then you
+#: have to perform two steps:
+#:
+#: * In the `update()` method of the responsible view/page/form
+#:   require the JavaScript code to be rendered into the page and set
+#:   tab for redirection::
+#:
+#:     from waeup.kofa.browser.resources import tabs
+#:
+#:     class MyPage(...):
+#:
+#:       def update(self):
+#:         tabs.need()
+#:         self.tab1 = self.tab2 = self.tab3 = ''
+#:         qs = self.request.get('QUERY_STRING', '')
+#:         if not qs:
+#:           qs = 'tab1'
+#:         setattr(self, qs, 'active')
+#:
+#:   This way all required JavaScripts will be rendered correctly
+#:   into the HTML page generated.
+#:
+#: * In your HTML code add some tags with like this:
+#:
+#:    .. code-block:: html
+#:
+#:     <ul class="tabs" data-tabs="tabs" >
+#:       <li tal:attributes="class view/tab1"><a href="#tab-1">Tab 1 Heading</a></li>
+#:       <li tal:attributes="class view/tab2"><a href="#tab-2">Tab 2 Heading</a></li>
+#:       <li tal:attributes="class view/tab3"><a href="#tab-3">Tab 3 Heading</a></li>
+#:     </ul>
+
+#:     <div class="tab-content">
+#:       <div id="tab-1" tal:attributes="class view/tab1">...</div>
+#:       <div id="tab-2" tal:attributes="class view/tab2">...</div>
+#:       <div id="tab-3" tal:attributes="class view/tab3">...</div>
+#:     </div>
+#:
+#: When the HTML is rendered, clicking into one of the tabs will make
+#: other <div>s disappear. See
+#:
+#:   http://twitter.github.com/bootstrap/javascript.html#tabs
+#:
+#: for details.
+#:
+tabs = ResourceInclusion(waeup_kofa, 'bootstrap-tabs-1.4.0.js',
+                          depends=[jquery])
+
+
+#: Resource that makes datatables for jQuery UI available.
+#:
+#: We register a normal version and a minified version. To make real
+#: use of datatables you might want to add some specialized CSS like
+#: `datatable_css` below.
+datatables = ResourceInclusion(waeup_kofa, 'jquery.dataTables.js',
+                               minified='jquery.dataTables.min.js',
+                               depends=[jquery])
+
+#: A stylesheet for datatables
+datatables_css = ResourceInclusion(waeup_kofa, 'datatables.css')
+
+#: A resource that turns HTML tables into sortable, searchable and :
+#: multi-page widgets using jQuery and the dataTables plugin available
+#: from http://www.datatables.net.
+#:
+#: If you want jQuery datatables for some <table> in your page then you
+#: have to perform two steps:
+#:
+#: * In the `update()` method of the responsible view/page/form
+#:   require the JavaScript code to be rendered into the page::
+#:
+#:     from waeup.kofa.browser.resources import datatable
+#:     # ...
+#:     class MyPage(...):
+#:       # ...
+#:       def update(self):
+#:         datatable.need()
+#:
+#:   This way all required JavaScripts will be rendered correctly
+#:   into the HTML page generated.
+#:
+#: * Assign a ``display dataTable`` class to the HTML table you want to tune:
+#:
+#:   .. code-block:: html
+#:
+#:      <table class="display dataTable">
+#:        <thead>
+#:          <tr>
+#:            <th>Name</th><th>Title</th>
+#:          </tr>
+#:        </thead>
+#:        <tbody>
+#:          <tr class="gradeA">
+#: 	      <td>Manfred</td><td>Mammoth of the year</td>
+#:          </tr>
+#:          <tr class="gradeA">
+#: 	      <td>Wolfgang</td><td>Junior</td>
+#:          </tr>
+#:        </tbody>
+#:      </table>
+#:
+#: When the HTML is rendered, clicking into one of the tabs will make
+#: other <div>s disappear. See
+#:
+#:   http://www.datatables.net/
+#:
+#: for details.
+#:
+datatable = ResourceInclusion(waeup_kofa, 'datatable.js',
+                              depends=[datatables, datatables_css])
+
+#: Register Twitter's Bootsrap css including dropdown js as a resource.
+bootstrap_min = ResourceInclusion(
+    waeup_kofa, 'bootstrap-1.4.0.css')
+
+dropdown = ResourceInclusion(
+    waeup_kofa, 'bootstrap-dropdown-1.4.0.js',
+    depends=[jquery])
+
+#: Register basic Kofa CSS (which is based on ``bootstrap.min-1.4.0.css``
+#: and ``base`` as a resource.
+waeup_base_css = ResourceInclusion(
+    waeup_kofa, 'waeup-base.css',
+    depends=[dropdown,bootstrap_min, base])
+
+#: A basic theme based on jQuery only (crappy yet).
+waeuptheme_empty = ResourceInclusion(
+    waeup_kofa, 'empty.css',
+    depends=[humanity])
+
+#: Render ReCaptcha with white theme.
+recaptcha_white = ResourceInclusion(
+    waeup_kofa, 'recaptcha_white.js'
+    )
+
+#: A page reloader (reloads after 3 secs)
+page_reloader = ResourceInclusion(
+    waeup_kofa, 'page_reloader.js', depends=[jquery])
+
+#: Disables page reloader (which is triggered by a JavaScript timer).
+page_not_reloader = ResourceInclusion(
+    waeup_kofa, 'page_not_reloader.js', depends=[jquery])
+
+loadbar = ResourceInclusion(
+    waeup_kofa, 'loadbar.js', depends=[jquery])
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/bootstrap-1.4.0.css
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/bootstrap-1.4.0.css	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/bootstrap-1.4.0.css	(revision 10886)
@@ -0,0 +1,2467 @@
+/*!
+ * Bootstrap v1.4.0
+ *
+ * Copyright 2011 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world @twitter by @mdo and @fat.
+ * Date: Sat Nov 12 01:04:51 PST 2011
+ */
+/* Reset.less
+ * Props to Eric Meyer (meyerweb.com) for his CSS reset file. We're using an adapted version here	that cuts out some of the reset HTML elements we will never need here (i.e., dfn, samp, etc).
+ * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */
+html, body {
+  margin: 0;
+  padding: 0;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+blockquote,
+pre,
+a,
+abbr,
+acronym,
+address,
+cite,
+code,
+del,
+dfn,
+em,
+img,
+q,
+s,
+samp,
+small,
+strike,
+strong,
+sub,
+sup,
+tt,
+var,
+dd,
+dl,
+dt,
+li,
+ol,
+ul,
+fieldset,
+form,
+label,
+legend,
+button,
+table,
+caption,
+tbody,
+tfoot,
+thead,
+tr,
+th,
+td {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  font-weight: normal;
+  font-style: normal;
+  font-size: 100%;
+  line-height: 1;
+  font-family: inherit;
+}
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+ol, ul {
+  list-style: none;
+}
+q:before,
+q:after,
+blockquote:before,
+blockquote:after {
+  content: "";
+}
+html {
+  overflow-y: scroll;
+  font-size: 100%;
+  -webkit-text-size-adjust: 100%;
+  -ms-text-size-adjust: 100%;
+}
+a:focus {
+  outline: thin dotted;
+}
+a:hover, a:active {
+  outline: 0;
+}
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+nav,
+section {
+  display: block;
+}
+audio, canvas, video {
+  display: inline-block;
+  *display: inline;
+  *zoom: 1;
+}
+audio:not([controls]) {
+  display: none;
+}
+sub, sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+sup {
+  top: -0.5em;
+}
+sub {
+  bottom: -0.25em;
+}
+img {
+  border: 0;
+  -ms-interpolation-mode: bicubic;
+}
+button,
+input,
+select,
+textarea {
+  font-size: 100%;
+  margin: 0;
+  vertical-align: baseline;
+  *vertical-align: middle;
+}
+button, input {
+  line-height: normal;
+  *overflow: visible;
+}
+button::-moz-focus-inner, input::-moz-focus-inner {
+  border: 0;
+  padding: 0;
+}
+button,
+input[type="button"],
+input[type="reset"],
+input[type="submit"] {
+  cursor: pointer;
+  -webkit-appearance: button;
+}
+input[type="search"] {
+  -webkit-appearance: textfield;
+  -webkit-box-sizing: content-box;
+  -moz-box-sizing: content-box;
+  box-sizing: content-box;
+}
+input[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+textarea {
+  overflow: auto;
+  vertical-align: top;
+}
+/* Variables.less
+ * Variables to customize the look and feel of Bootstrap
+ * ----------------------------------------------------- */
+/* Mixins.less
+ * Snippets of reusable CSS to develop faster and keep code readable
+ * ----------------------------------------------------------------- */
+/*
+ * Scaffolding
+ * Basic and global styles for generating a grid system, structural layout, and page templates
+ * ------------------------------------------------------------------------------------------- */
+body {
+  background-color: #ffffff;
+  margin: 0;
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: 13px;
+  font-weight: normal;
+  line-height: 18px;
+  color: #404040;
+}
+.container {
+  width: 940px;
+  margin-left: auto;
+  margin-right: auto;
+  zoom: 1;
+}
+.container:before, .container:after {
+  display: table;
+  content: "";
+  zoom: 1;
+}
+.container:after {
+  clear: both;
+}
+.container-fluid {
+  position: relative;
+  min-width: 940px;
+  padding-left: 20px;
+  padding-right: 20px;
+  zoom: 1;
+}
+.container-fluid:before, .container-fluid:after {
+  display: table;
+  content: "";
+  zoom: 1;
+}
+.container-fluid:after {
+  clear: both;
+}
+.container-fluid > .sidebar {
+  position: absolute;
+  top: 0;
+  left: 20px;
+  width: 220px;
+}
+.container-fluid > .content {
+  margin-left: 240px;
+}
+a {
+  color: #0069d6;
+  text-decoration: none;
+  line-height: inherit;
+  font-weight: inherit;
+}
+a:hover {
+  color: #00438a;
+  text-decoration: underline;
+}
+.pull-right {
+  float: right;
+}
+.pull-left {
+  float: left;
+}
+.hide {
+  display: none;
+}
+.show {
+  display: block;
+}
+.row {
+  zoom: 1;
+  margin-left: -20px;
+}
+.row:before, .row:after {
+  display: table;
+  content: "";
+  zoom: 1;
+}
+.row:after {
+  clear: both;
+}
+.row > [class*="span"] {
+  display: inline;
+  float: left;
+  margin-left: 20px;
+}
+.span1 {
+  width: 40px;
+}
+.span2 {
+  width: 100px;
+}
+.span3 {
+  width: 160px;
+}
+.span4 {
+  width: 220px;
+}
+.span5 {
+  width: 280px;
+}
+.span6 {
+  width: 340px;
+}
+.span7 {
+  width: 400px;
+}
+.span8 {
+  width: 460px;
+}
+.span9 {
+  width: 520px;
+}
+.span10 {
+  width: 580px;
+}
+.span11 {
+  width: 640px;
+}
+.span12 {
+  width: 700px;
+}
+.span13 {
+  width: 760px;
+}
+.span14 {
+  width: 820px;
+}
+.span15 {
+  width: 880px;
+}
+.span16 {
+  width: 940px;
+}
+.span17 {
+  width: 1000px;
+}
+.span18 {
+  width: 1060px;
+}
+.span19 {
+  width: 1120px;
+}
+.span20 {
+  width: 1180px;
+}
+.span21 {
+  width: 1240px;
+}
+.span22 {
+  width: 1300px;
+}
+.span23 {
+  width: 1360px;
+}
+.span24 {
+  width: 1420px;
+}
+.row > .offset1 {
+  margin-left: 80px;
+}
+.row > .offset2 {
+  margin-left: 140px;
+}
+.row > .offset3 {
+  margin-left: 200px;
+}
+.row > .offset4 {
+  margin-left: 260px;
+}
+.row > .offset5 {
+  margin-left: 320px;
+}
+.row > .offset6 {
+  margin-left: 380px;
+}
+.row > .offset7 {
+  margin-left: 440px;
+}
+.row > .offset8 {
+  margin-left: 500px;
+}
+.row > .offset9 {
+  margin-left: 560px;
+}
+.row > .offset10 {
+  margin-left: 620px;
+}
+.row > .offset11 {
+  margin-left: 680px;
+}
+.row > .offset12 {
+  margin-left: 740px;
+}
+.span-one-third {
+  width: 300px;
+}
+.span-two-thirds {
+  width: 620px;
+}
+.row > .offset-one-third {
+  margin-left: 340px;
+}
+.row > .offset-two-thirds {
+  margin-left: 660px;
+}
+/* Typography.less
+ * Headings, body text, lists, code, and more for a versatile and durable typography system
+ * ---------------------------------------------------------------------------------------- */
+p {
+  font-size: 13px;
+  font-weight: normal;
+  line-height: 18px;
+  margin-bottom: 9px;
+}
+p small {
+  font-size: 11px;
+  color: #bfbfbf;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-weight: bold;
+  color: #404040;
+}
+h1 small,
+h2 small,
+h3 small,
+h4 small,
+h5 small,
+h6 small {
+  color: #bfbfbf;
+}
+h1 {
+  margin-bottom: 18px;
+  font-size: 30px;
+  line-height: 36px;
+}
+h1 small {
+  font-size: 18px;
+}
+h2 {
+  font-size: 24px;
+  line-height: 36px;
+}
+h2 small {
+  font-size: 14px;
+}
+h3,
+h4,
+h5,
+h6 {
+  line-height: 36px;
+}
+h3 {
+  font-size: 18px;
+}
+h3 small {
+  font-size: 14px;
+}
+h4 {
+  font-size: 16px;
+}
+h4 small {
+  font-size: 12px;
+}
+h5 {
+  font-size: 14px;
+}
+h6 {
+  font-size: 13px;
+  color: #bfbfbf;
+  text-transform: uppercase;
+}
+ul, ol {
+  margin: 0 0 18px 25px;
+}
+ul ul,
+ul ol,
+ol ol,
+ol ul {
+  margin-bottom: 0;
+}
+ul {
+  list-style: disc;
+}
+ol {
+  list-style: decimal;
+}
+li {
+  line-height: 18px;
+  color: #808080;
+}
+ul.unstyled {
+  list-style: none;
+  margin-left: 0;
+}
+dl {
+  margin-bottom: 18px;
+}
+dl dt, dl dd {
+  line-height: 18px;
+}
+dl dt {
+  font-weight: bold;
+}
+dl dd {
+  margin-left: 9px;
+}
+hr {
+  margin: 20px 0 19px;
+  border: 0;
+  border-bottom: 1px solid #eee;
+}
+strong {
+  font-style: inherit;
+  font-weight: bold;
+}
+em {
+  font-style: italic;
+  font-weight: inherit;
+  line-height: inherit;
+}
+.muted {
+  color: #bfbfbf;
+}
+blockquote {
+  margin-bottom: 18px;
+  border-left: 5px solid #eee;
+  padding-left: 15px;
+}
+blockquote p {
+  font-size: 14px;
+  font-weight: 300;
+  line-height: 18px;
+  margin-bottom: 0;
+}
+blockquote small {
+  display: block;
+  font-size: 12px;
+  font-weight: 300;
+  line-height: 18px;
+  color: #bfbfbf;
+}
+blockquote small:before {
+  content: '\2014 \00A0';
+}
+address {
+  display: block;
+  line-height: 18px;
+  margin-bottom: 18px;
+}
+code, pre {
+  padding: 0 3px 2px;
+  font-family: Monaco, Andale Mono, Courier New, monospace;
+  font-size: 12px;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+}
+code {
+  background-color: #fee9cc;
+  color: rgba(0, 0, 0, 0.75);
+  padding: 1px 3px;
+}
+pre {
+  background-color: #f5f5f5;
+  display: block;
+  padding: 8.5px;
+  margin: 0 0 18px;
+  line-height: 18px;
+  font-size: 12px;
+  border: 1px solid #ccc;
+  border: 1px solid rgba(0, 0, 0, 0.15);
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+  white-space: pre;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+}
+/* Forms.less
+ * Base styles for various input types, form layouts, and states
+ * ------------------------------------------------------------- */
+form {
+  margin-bottom: 18px;
+}
+fieldset {
+  margin-bottom: 18px;
+  padding-top: 18px;
+}
+fieldset legend {
+  display: block;
+  padding-left: 150px;
+  font-size: 19.5px;
+  line-height: 1;
+  color: #404040;
+  *padding: 0 0 5px 145px;
+  /* IE6-7 */
+
+  *line-height: 1.5;
+  /* IE6-7 */
+
+}
+form .clearfix {
+  margin-bottom: 18px;
+  zoom: 1;
+}
+form .clearfix:before, form .clearfix:after {
+  display: table;
+  content: "";
+  zoom: 1;
+}
+form .clearfix:after {
+  clear: both;
+}
+label,
+input,
+select,
+textarea {
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: 13px;
+  font-weight: normal;
+  line-height: normal;
+}
+label {
+  padding-top: 6px;
+  font-size: 13px;
+  line-height: 18px;
+  float: left;
+  width: 130px;
+  text-align: right;
+  color: #404040;
+}
+form .input {
+  margin-left: 150px;
+}
+input[type=checkbox], input[type=radio] {
+  cursor: pointer;
+}
+input,
+textarea,
+select,
+.uneditable-input {
+  display: inline-block;
+  width: 210px;
+  height: 18px;
+  padding: 4px;
+  font-size: 13px;
+  line-height: 18px;
+  color: #808080;
+  border: 1px solid #ccc;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+}
+select {
+  padding: initial;
+}
+input[type=checkbox], input[type=radio] {
+  width: auto;
+  height: auto;
+  padding: 0;
+  margin: 3px 0;
+  *margin-top: 0;
+  /* IE6-7 */
+
+  line-height: normal;
+  border: none;
+}
+input[type=file] {
+  background-color: #ffffff;
+  padding: initial;
+  border: initial;
+  line-height: initial;
+  -webkit-box-shadow: none;
+  -moz-box-shadow: none;
+  box-shadow: none;
+}
+input[type=button], input[type=reset], input[type=submit] {
+  width: auto;
+  height: auto;
+}
+select, input[type=file] {
+  height: 27px;
+  *height: auto;
+  line-height: 27px;
+  *margin-top: 4px;
+  /* For IE7, add top margin to align select with labels */
+
+}
+select[multiple] {
+  height: inherit;
+  background-color: #ffffff;
+}
+textarea {
+  height: auto;
+}
+.uneditable-input {
+  background-color: #ffffff;
+  display: block;
+  border-color: #eee;
+  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
+  -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
+  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
+  cursor: not-allowed;
+}
+:-moz-placeholder {
+  color: #bfbfbf;
+}
+::-webkit-input-placeholder {
+  color: #bfbfbf;
+}
+input, textarea {
+  -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
+  -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
+  -ms-transition: border linear 0.2s, box-shadow linear 0.2s;
+  -o-transition: border linear 0.2s, box-shadow linear 0.2s;
+  transition: border linear 0.2s, box-shadow linear 0.2s;
+  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+  -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+input:focus, textarea:focus {
+  outline: 0;
+  border-color: rgba(82, 168, 236, 0.8);
+  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+  -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+}
+input[type=file]:focus, input[type=checkbox]:focus, select:focus {
+  -webkit-box-shadow: none;
+  -moz-box-shadow: none;
+  box-shadow: none;
+  outline: 1px dotted #666;
+}
+form .clearfix.error > label, form .clearfix.error .help-block, form .clearfix.error .help-inline {
+  color: #b94a48;
+}
+form .clearfix.error input, form .clearfix.error textarea {
+  color: #b94a48;
+  border-color: #ee5f5b;
+}
+form .clearfix.error input:focus, form .clearfix.error textarea:focus {
+  border-color: #e9322d;
+  -webkit-box-shadow: 0 0 6px #f8b9b7;
+  -moz-box-shadow: 0 0 6px #f8b9b7;
+  box-shadow: 0 0 6px #f8b9b7;
+}
+form .clearfix.error .input-prepend .add-on, form .clearfix.error .input-append .add-on {
+  color: #b94a48;
+  background-color: #fce6e6;
+  border-color: #b94a48;
+}
+form .clearfix.warning > label, form .clearfix.warning .help-block, form .clearfix.warning .help-inline {
+  color: #c09853;
+}
+form .clearfix.warning input, form .clearfix.warning textarea {
+  color: #c09853;
+  border-color: #ccae64;
+}
+form .clearfix.warning input:focus, form .clearfix.warning textarea:focus {
+  border-color: #be9a3f;
+  -webkit-box-shadow: 0 0 6px #e5d6b1;
+  -moz-box-shadow: 0 0 6px #e5d6b1;
+  box-shadow: 0 0 6px #e5d6b1;
+}
+form .clearfix.warning .input-prepend .add-on, form .clearfix.warning .input-append .add-on {
+  color: #c09853;
+  background-color: #d2b877;
+  border-color: #c09853;
+}
+form .clearfix.success > label, form .clearfix.success .help-block, form .clearfix.success .help-inline {
+  color: #468847;
+}
+form .clearfix.success input, form .clearfix.success textarea {
+  color: #468847;
+  border-color: #57a957;
+}
+form .clearfix.success input:focus, form .clearfix.success textarea:focus {
+  border-color: #458845;
+  -webkit-box-shadow: 0 0 6px #9acc9a;
+  -moz-box-shadow: 0 0 6px #9acc9a;
+  box-shadow: 0 0 6px #9acc9a;
+}
+form .clearfix.success .input-prepend .add-on, form .clearfix.success .input-append .add-on {
+  color: #468847;
+  background-color: #bcddbc;
+  border-color: #468847;
+}
+.input-mini,
+input.mini,
+textarea.mini,
+select.mini {
+  width: 60px;
+}
+.input-small,
+input.small,
+textarea.small,
+select.small {
+  width: 90px;
+}
+.input-medium,
+input.medium,
+textarea.medium,
+select.medium {
+  width: 150px;
+}
+.input-large,
+input.large,
+textarea.large,
+select.large {
+  width: 210px;
+}
+.input-xlarge,
+input.xlarge,
+textarea.xlarge,
+select.xlarge {
+  width: 270px;
+}
+.input-xxlarge,
+input.xxlarge,
+textarea.xxlarge,
+select.xxlarge {
+  width: 530px;
+}
+textarea.xxlarge {
+  overflow-y: auto;
+}
+input.span1, textarea.span1 {
+  display: inline-block;
+  float: none;
+  width: 30px;
+  margin-left: 0;
+}
+input.span2, textarea.span2 {
+  display: inline-block;
+  float: none;
+  width: 90px;
+  margin-left: 0;
+}
+input.span3, textarea.span3 {
+  display: inline-block;
+  float: none;
+  width: 150px;
+  margin-left: 0;
+}
+input.span4, textarea.span4 {
+  display: inline-block;
+  float: none;
+  width: 210px;
+  margin-left: 0;
+}
+input.span5, textarea.span5 {
+  display: inline-block;
+  float: none;
+  width: 270px;
+  margin-left: 0;
+}
+input.span6, textarea.span6 {
+  display: inline-block;
+  float: none;
+  width: 330px;
+  margin-left: 0;
+}
+input.span7, textarea.span7 {
+  display: inline-block;
+  float: none;
+  width: 390px;
+  margin-left: 0;
+}
+input.span8, textarea.span8 {
+  display: inline-block;
+  float: none;
+  width: 450px;
+  margin-left: 0;
+}
+input.span9, textarea.span9 {
+  display: inline-block;
+  float: none;
+  width: 510px;
+  margin-left: 0;
+}
+input.span10, textarea.span10 {
+  display: inline-block;
+  float: none;
+  width: 570px;
+  margin-left: 0;
+}
+input.span11, textarea.span11 {
+  display: inline-block;
+  float: none;
+  width: 630px;
+  margin-left: 0;
+}
+input.span12, textarea.span12 {
+  display: inline-block;
+  float: none;
+  width: 690px;
+  margin-left: 0;
+}
+input.span13, textarea.span13 {
+  display: inline-block;
+  float: none;
+  width: 750px;
+  margin-left: 0;
+}
+input.span14, textarea.span14 {
+  display: inline-block;
+  float: none;
+  width: 810px;
+  margin-left: 0;
+}
+input.span15, textarea.span15 {
+  display: inline-block;
+  float: none;
+  width: 870px;
+  margin-left: 0;
+}
+input.span16, textarea.span16 {
+  display: inline-block;
+  float: none;
+  width: 930px;
+  margin-left: 0;
+}
+input[disabled],
+select[disabled],
+textarea[disabled],
+input[readonly],
+select[readonly],
+textarea[readonly] {
+  background-color: #f5f5f5;
+  border-color: #ddd;
+  cursor: not-allowed;
+}
+.actions {
+  background: #f5f5f5;
+  margin-top: 18px;
+  margin-bottom: 18px;
+  padding: 17px 20px 18px 150px;
+  border-top: 1px solid #ddd;
+  -webkit-border-radius: 0 0 3px 3px;
+  -moz-border-radius: 0 0 3px 3px;
+  border-radius: 0 0 3px 3px;
+}
+.actions .secondary-action {
+  float: right;
+}
+.actions .secondary-action a {
+  line-height: 30px;
+}
+.actions .secondary-action a:hover {
+  text-decoration: underline;
+}
+.help-inline, .help-block {
+  font-size: 13px;
+  line-height: 18px;
+  color: #bfbfbf;
+}
+.help-inline {
+  padding-left: 5px;
+  *position: relative;
+  /* IE6-7 */
+
+  *top: -5px;
+  /* IE6-7 */
+
+}
+.help-block {
+  display: block;
+  max-width: 600px;
+}
+.inline-inputs {
+  color: #808080;
+}
+.inline-inputs span {
+  padding: 0 2px 0 1px;
+}
+.input-prepend input, .input-append input {
+  -webkit-border-radius: 0 3px 3px 0;
+  -moz-border-radius: 0 3px 3px 0;
+  border-radius: 0 3px 3px 0;
+}
+.input-prepend .add-on, .input-append .add-on {
+  position: relative;
+  background: #f5f5f5;
+  border: 1px solid #ccc;
+  z-index: 2;
+  float: left;
+  display: block;
+  width: auto;
+  min-width: 16px;
+  height: 18px;
+  padding: 4px 4px 4px 5px;
+  margin-right: -1px;
+  font-weight: normal;
+  line-height: 18px;
+  color: #bfbfbf;
+  text-align: center;
+  text-shadow: 0 1px 0 #ffffff;
+  -webkit-border-radius: 3px 0 0 3px;
+  -moz-border-radius: 3px 0 0 3px;
+  border-radius: 3px 0 0 3px;
+}
+.input-prepend .active, .input-append .active {
+  background: #a9dba9;
+  border-color: #46a546;
+}
+.input-prepend .add-on {
+  *margin-top: 1px;
+  /* IE6-7 */
+
+}
+.input-append input {
+  float: left;
+  -webkit-border-radius: 3px 0 0 3px;
+  -moz-border-radius: 3px 0 0 3px;
+  border-radius: 3px 0 0 3px;
+}
+.input-append .add-on {
+  -webkit-border-radius: 0 3px 3px 0;
+  -moz-border-radius: 0 3px 3px 0;
+  border-radius: 0 3px 3px 0;
+  margin-right: 0;
+  margin-left: -1px;
+}
+.inputs-list {
+  margin: 0 0 5px;
+  width: 100%;
+}
+.inputs-list li {
+  display: block;
+  padding: 0;
+  width: 100%;
+}
+.inputs-list label {
+  display: block;
+  float: none;
+  width: auto;
+  padding: 0;
+  margin-left: 20px;
+  line-height: 18px;
+  text-align: left;
+  white-space: normal;
+}
+.inputs-list label strong {
+  color: #808080;
+}
+.inputs-list label small {
+  font-size: 11px;
+  font-weight: normal;
+}
+.inputs-list .inputs-list {
+  margin-left: 25px;
+  margin-bottom: 10px;
+  padding-top: 0;
+}
+.inputs-list:first-child {
+  padding-top: 6px;
+}
+.inputs-list li + li {
+  padding-top: 2px;
+}
+.inputs-list input[type=radio], .inputs-list input[type=checkbox] {
+  margin-bottom: 0;
+  margin-left: -20px;
+  float: left;
+}
+.form-stacked {
+  padding-left: 20px;
+}
+.form-stacked fieldset {
+  padding-top: 9px;
+}
+.form-stacked legend {
+  padding-left: 0;
+}
+.form-stacked label {
+  display: block;
+  float: none;
+  width: auto;
+  font-weight: bold;
+  text-align: left;
+  line-height: 20px;
+  padding-top: 0;
+}
+.form-stacked .clearfix {
+  margin-bottom: 9px;
+}
+.form-stacked .clearfix div.input {
+  margin-left: 0;
+}
+.form-stacked .inputs-list {
+  margin-bottom: 0;
+}
+.form-stacked .inputs-list li {
+  padding-top: 0;
+}
+.form-stacked .inputs-list li label {
+  font-weight: normal;
+  padding-top: 0;
+}
+.form-stacked div.clearfix.error {
+  padding-top: 10px;
+  padding-bottom: 10px;
+  padding-left: 10px;
+  margin-top: 0;
+  margin-left: -10px;
+}
+.form-stacked .actions {
+  margin-left: -20px;
+  padding-left: 20px;
+}
+/*
+ * Tables.less
+ * Tables for, you guessed it, tabular data
+ * ---------------------------------------- */
+table {
+  width: 100%;
+  margin-bottom: 18px;
+  padding: 0;
+  font-size: 13px;
+  border-collapse: collapse;
+}
+table th, table td {
+  padding: 10px 10px 9px;
+  line-height: 18px;
+  text-align: left;
+}
+table th {
+  padding-top: 9px;
+  font-weight: bold;
+  vertical-align: middle;
+}
+table td {
+  vertical-align: top;
+  border-top: 1px solid #ddd;
+}
+table tbody th {
+  border-top: 1px solid #ddd;
+  vertical-align: top;
+}
+.condensed-table th, .condensed-table td {
+  padding: 5px 5px 4px;
+}
+.bordered-table {
+  border: 1px solid #ddd;
+  border-collapse: separate;
+  *border-collapse: collapse;
+  /* IE7, collapse table to remove spacing */
+
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.bordered-table th + th, .bordered-table td + td, .bordered-table th + td {
+  border-left: 1px solid #ddd;
+}
+.bordered-table thead tr:first-child th:first-child, .bordered-table tbody tr:first-child td:first-child {
+  -webkit-border-radius: 4px 0 0 0;
+  -moz-border-radius: 4px 0 0 0;
+  border-radius: 4px 0 0 0;
+}
+.bordered-table thead tr:first-child th:last-child, .bordered-table tbody tr:first-child td:last-child {
+  -webkit-border-radius: 0 4px 0 0;
+  -moz-border-radius: 0 4px 0 0;
+  border-radius: 0 4px 0 0;
+}
+.bordered-table tbody tr:last-child td:first-child {
+  -webkit-border-radius: 0 0 0 4px;
+  -moz-border-radius: 0 0 0 4px;
+  border-radius: 0 0 0 4px;
+}
+.bordered-table tbody tr:last-child td:last-child {
+  -webkit-border-radius: 0 0 4px 0;
+  -moz-border-radius: 0 0 4px 0;
+  border-radius: 0 0 4px 0;
+}
+table .span1 {
+  width: 20px;
+}
+table .span2 {
+  width: 60px;
+}
+table .span3 {
+  width: 100px;
+}
+table .span4 {
+  width: 140px;
+}
+table .span5 {
+  width: 180px;
+}
+table .span6 {
+  width: 220px;
+}
+table .span7 {
+  width: 260px;
+}
+table .span8 {
+  width: 300px;
+}
+table .span9 {
+  width: 340px;
+}
+table .span10 {
+  width: 380px;
+}
+table .span11 {
+  width: 420px;
+}
+table .span12 {
+  width: 460px;
+}
+table .span13 {
+  width: 500px;
+}
+table .span14 {
+  width: 540px;
+}
+table .span15 {
+  width: 580px;
+}
+table .span16 {
+  width: 620px;
+}
+.zebra-striped tbody tr:nth-child(odd) td, .zebra-striped tbody tr:nth-child(odd) th {
+  background-color: #f9f9f9;
+}
+.zebra-striped tbody tr:hover td, .zebra-striped tbody tr:hover th {
+  background-color: #f5f5f5;
+}
+table .header {
+  cursor: pointer;
+}
+table .header:after {
+  content: "";
+  float: right;
+  margin-top: 7px;
+  border-width: 0 4px 4px;
+  border-style: solid;
+  border-color: #000 transparent;
+  visibility: hidden;
+}
+table .headerSortUp, table .headerSortDown {
+  background-color: rgba(141, 192, 219, 0.25);
+  text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
+}
+table .header:hover:after {
+  visibility: visible;
+}
+table .headerSortDown:after, table .headerSortDown:hover:after {
+  visibility: visible;
+  filter: alpha(opacity=60);
+  -khtml-opacity: 0.6;
+  -moz-opacity: 0.6;
+  opacity: 0.6;
+}
+table .headerSortUp:after {
+  border-bottom: none;
+  border-left: 4px solid transparent;
+  border-right: 4px solid transparent;
+  border-top: 4px solid #000;
+  visibility: visible;
+  -webkit-box-shadow: none;
+  -moz-box-shadow: none;
+  box-shadow: none;
+  filter: alpha(opacity=60);
+  -khtml-opacity: 0.6;
+  -moz-opacity: 0.6;
+  opacity: 0.6;
+}
+table .blue {
+  color: #049cdb;
+  border-bottom-color: #049cdb;
+}
+table .headerSortUp.blue, table .headerSortDown.blue {
+  background-color: #ade6fe;
+}
+table .green {
+  color: #46a546;
+  border-bottom-color: #46a546;
+}
+table .headerSortUp.green, table .headerSortDown.green {
+  background-color: #cdeacd;
+}
+table .red {
+  color: #9d261d;
+  border-bottom-color: #9d261d;
+}
+table .headerSortUp.red, table .headerSortDown.red {
+  background-color: #f4c8c5;
+}
+table .yellow {
+  color: #ffc40d;
+  border-bottom-color: #ffc40d;
+}
+table .headerSortUp.yellow, table .headerSortDown.yellow {
+  background-color: #fff6d9;
+}
+table .orange {
+  color: #f89406;
+  border-bottom-color: #f89406;
+}
+table .headerSortUp.orange, table .headerSortDown.orange {
+  background-color: #fee9cc;
+}
+table .purple {
+  color: #7a43b6;
+  border-bottom-color: #7a43b6;
+}
+table .headerSortUp.purple, table .headerSortDown.purple {
+  background-color: #e2d5f0;
+}
+/* Patterns.less
+ * Repeatable UI elements outside the base styles provided from the scaffolding
+ * ---------------------------------------------------------------------------- */
+.topbar {
+  height: 40px;
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 10000;
+  overflow: visible;
+}
+.topbar a {
+  color: #bfbfbf;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.topbar h3 a:hover, .topbar .brand:hover, .topbar ul .active > a {
+  background-color: #333;
+  background-color: rgba(255, 255, 255, 0.05);
+  color: #ffffff;
+  text-decoration: none;
+}
+.topbar h3 {
+  position: relative;
+}
+.topbar h3 a, .topbar .brand {
+  float: left;
+  display: block;
+  padding: 8px 20px 12px;
+  margin-left: -20px;
+  color: #ffffff;
+  font-size: 20px;
+  font-weight: 200;
+  line-height: 1;
+}
+.topbar p {
+  margin: 0;
+  line-height: 40px;
+}
+.topbar p a:hover {
+  background-color: transparent;
+  color: #ffffff;
+}
+.topbar form {
+  float: left;
+  margin: 5px 0 0 0;
+  position: relative;
+  filter: alpha(opacity=100);
+  -khtml-opacity: 1;
+  -moz-opacity: 1;
+  opacity: 1;
+}
+.topbar form.pull-right {
+  float: right;
+}
+.topbar input {
+  background-color: #444;
+  background-color: rgba(255, 255, 255, 0.3);
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: normal;
+  font-weight: 13px;
+  line-height: 1;
+  padding: 4px 9px;
+  color: #ffffff;
+  color: rgba(255, 255, 255, 0.75);
+  border: 1px solid #111;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.25);
+  -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.25);
+  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.25);
+  -webkit-transition: none;
+  -moz-transition: none;
+  -ms-transition: none;
+  -o-transition: none;
+  transition: none;
+}
+.topbar input:-moz-placeholder {
+  color: #e6e6e6;
+}
+.topbar input::-webkit-input-placeholder {
+  color: #e6e6e6;
+}
+.topbar input:hover {
+  background-color: #bfbfbf;
+  background-color: rgba(255, 255, 255, 0.5);
+  color: #ffffff;
+}
+.topbar input:focus, .topbar input.focused {
+  outline: 0;
+  background-color: #ffffff;
+  color: #404040;
+  text-shadow: 0 1px 0 #ffffff;
+  border: 0;
+  padding: 5px 10px;
+  -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
+  -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
+  box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
+}
+.topbar-inner, .topbar .fill {
+  background-color: #222;
+  background-color: #222222;
+  background-repeat: repeat-x;
+  background-image: -khtml-gradient(linear, left top, left bottom, from(#333333), to(#222222));
+  background-image: -moz-linear-gradient(top, #333333, #222222);
+  background-image: -ms-linear-gradient(top, #333333, #222222);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #333333), color-stop(100%, #222222));
+  background-image: -webkit-linear-gradient(top, #333333, #222222);
+  background-image: -o-linear-gradient(top, #333333, #222222);
+  background-image: linear-gradient(top, #333333, #222222);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);
+  -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
+  -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
+}
+.topbar div > ul, .nav {
+  display: block;
+  float: left;
+  margin: 0 10px 0 0;
+  position: relative;
+  left: 0;
+}
+.topbar div > ul > li, .nav > li {
+  display: block;
+  float: left;
+}
+.topbar div > ul a, .nav a {
+  display: block;
+  float: none;
+  padding: 10px 10px 11px;
+  line-height: 19px;
+  text-decoration: none;
+}
+.topbar div > ul a:hover, .nav a:hover {
+  color: #ffffff;
+  text-decoration: none;
+}
+.topbar div > ul .active > a, .nav .active > a {
+  background-color: #222;
+  background-color: rgba(0, 0, 0, 0.5);
+}
+.topbar div > ul.secondary-nav, .nav.secondary-nav {
+  float: right;
+  margin-left: 10px;
+  margin-right: 0;
+}
+.topbar div > ul.secondary-nav .menu-dropdown,
+.nav.secondary-nav .menu-dropdown,
+.topbar div > ul.secondary-nav .dropdown-menu,
+.nav.secondary-nav .dropdown-menu {
+  right: 0;
+  border: 0;
+}
+.topbar div > ul a.menu:hover,
+.nav a.menu:hover,
+.topbar div > ul li.open .menu,
+.nav li.open .menu,
+.topbar div > ul .dropdown-toggle:hover,
+.nav .dropdown-toggle:hover,
+.topbar div > ul .dropdown.open .dropdown-toggle,
+.nav .dropdown.open .dropdown-toggle {
+  background: #444;
+  background: rgba(255, 255, 255, 0.05);
+}
+.topbar div > ul .menu-dropdown,
+.nav .menu-dropdown,
+.topbar div > ul .dropdown-menu,
+.nav .dropdown-menu {
+  background-color: #333;
+}
+.topbar div > ul .menu-dropdown a.menu,
+.nav .menu-dropdown a.menu,
+.topbar div > ul .dropdown-menu a.menu,
+.nav .dropdown-menu a.menu,
+.topbar div > ul .menu-dropdown .dropdown-toggle,
+.nav .menu-dropdown .dropdown-toggle,
+.topbar div > ul .dropdown-menu .dropdown-toggle,
+.nav .dropdown-menu .dropdown-toggle {
+  color: #ffffff;
+}
+.topbar div > ul .menu-dropdown a.menu.open,
+.nav .menu-dropdown a.menu.open,
+.topbar div > ul .dropdown-menu a.menu.open,
+.nav .dropdown-menu a.menu.open,
+.topbar div > ul .menu-dropdown .dropdown-toggle.open,
+.nav .menu-dropdown .dropdown-toggle.open,
+.topbar div > ul .dropdown-menu .dropdown-toggle.open,
+.nav .dropdown-menu .dropdown-toggle.open {
+  background: #444;
+  background: rgba(255, 255, 255, 0.05);
+}
+.topbar div > ul .menu-dropdown li a,
+.nav .menu-dropdown li a,
+.topbar div > ul .dropdown-menu li a,
+.nav .dropdown-menu li a {
+  color: #999;
+  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
+}
+.topbar div > ul .menu-dropdown li a:hover,
+.nav .menu-dropdown li a:hover,
+.topbar div > ul .dropdown-menu li a:hover,
+.nav .dropdown-menu li a:hover {
+  background-color: #191919;
+  background-repeat: repeat-x;
+  background-image: -khtml-gradient(linear, left top, left bottom, from(#292929), to(#191919));
+  background-image: -moz-linear-gradient(top, #292929, #191919);
+  background-image: -ms-linear-gradient(top, #292929, #191919);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #292929), color-stop(100%, #191919));
+  background-image: -webkit-linear-gradient(top, #292929, #191919);
+  background-image: -o-linear-gradient(top, #292929, #191919);
+  background-image: linear-gradient(top, #292929, #191919);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#292929', endColorstr='#191919', GradientType=0);
+  color: #ffffff;
+}
+.topbar div > ul .menu-dropdown .active a,
+.nav .menu-dropdown .active a,
+.topbar div > ul .dropdown-menu .active a,
+.nav .dropdown-menu .active a {
+  color: #ffffff;
+}
+.topbar div > ul .menu-dropdown .divider,
+.nav .menu-dropdown .divider,
+.topbar div > ul .dropdown-menu .divider,
+.nav .dropdown-menu .divider {
+  background-color: #222;
+  border-color: #444;
+}
+.topbar ul .menu-dropdown li a, .topbar ul .dropdown-menu li a {
+  padding: 4px 15px;
+}
+li.menu, .dropdown {
+  position: relative;
+}
+a.menu:after, .dropdown-toggle:after {
+  width: 0;
+  height: 0;
+  display: inline-block;
+  content: "&darr;";
+  text-indent: -99999px;
+  vertical-align: top;
+  margin-top: 8px;
+  margin-left: 4px;
+  border-left: 4px solid transparent;
+  border-right: 4px solid transparent;
+  border-top: 4px solid #ffffff;
+  filter: alpha(opacity=50);
+  -khtml-opacity: 0.5;
+  -moz-opacity: 0.5;
+  opacity: 0.5;
+}
+.menu-dropdown, .dropdown-menu {
+  background-color: #ffffff;
+  float: left;
+  display: none;
+  position: absolute;
+  top: 40px;
+  z-index: 900;
+  min-width: 160px;
+  max-width: 220px;
+  _width: 160px;
+  margin-left: 0;
+  margin-right: 0;
+  padding: 6px 0;
+  zoom: 1;
+  border-color: #999;
+  border-color: rgba(0, 0, 0, 0.2);
+  border-style: solid;
+  border-width: 0 1px 1px;
+  -webkit-border-radius: 0 0 6px 6px;
+  -moz-border-radius: 0 0 6px 6px;
+  border-radius: 0 0 6px 6px;
+  -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+  -moz-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+  -webkit-background-clip: padding-box;
+  -moz-background-clip: padding-box;
+  background-clip: padding-box;
+}
+.menu-dropdown li, .dropdown-menu li {
+  float: none;
+  display: block;
+  background-color: none;
+}
+.menu-dropdown .divider, .dropdown-menu .divider {
+  height: 1px;
+  margin: 5px 0;
+  overflow: hidden;
+  background-color: #eee;
+  border-bottom: 1px solid #ffffff;
+}
+.topbar .dropdown-menu a, .dropdown-menu a {
+  display: block;
+  padding: 4px 15px;
+  clear: both;
+  font-weight: normal;
+  line-height: 18px;
+  color: #808080;
+  text-shadow: 0 1px 0 #ffffff;
+}
+.topbar .dropdown-menu a:hover,
+.dropdown-menu a:hover,
+.topbar .dropdown-menu a.hover,
+.dropdown-menu a.hover {
+  background-color: #dddddd;
+  background-repeat: repeat-x;
+  background-image: -khtml-gradient(linear, left top, left bottom, from(#eeeeee), to(#dddddd));
+  background-image: -moz-linear-gradient(top, #eeeeee, #dddddd);
+  background-image: -ms-linear-gradient(top, #eeeeee, #dddddd);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #eeeeee), color-stop(100%, #dddddd));
+  background-image: -webkit-linear-gradient(top, #eeeeee, #dddddd);
+  background-image: -o-linear-gradient(top, #eeeeee, #dddddd);
+  background-image: linear-gradient(top, #eeeeee, #dddddd);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#dddddd', GradientType=0);
+  color: #404040;
+  text-decoration: none;
+  -webkit-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.025), inset 0 -1px rgba(0, 0, 0, 0.025);
+  -moz-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.025), inset 0 -1px rgba(0, 0, 0, 0.025);
+  box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.025), inset 0 -1px rgba(0, 0, 0, 0.025);
+}
+.open .menu,
+.dropdown.open .menu,
+.open .dropdown-toggle,
+.dropdown.open .dropdown-toggle {
+  color: #ffffff;
+  background: #ccc;
+  background: rgba(0, 0, 0, 0.3);
+}
+.open .menu-dropdown,
+.dropdown.open .menu-dropdown,
+.open .dropdown-menu,
+.dropdown.open .dropdown-menu {
+  display: block;
+}
+.tabs, .pills {
+  margin: 0 0 18px;
+  padding: 0;
+  list-style: none;
+  zoom: 1;
+}
+.tabs:before,
+.pills:before,
+.tabs:after,
+.pills:after {
+  display: table;
+  content: "";
+  zoom: 1;
+}
+.tabs:after, .pills:after {
+  clear: both;
+}
+.tabs > li, .pills > li {
+  float: left;
+}
+.tabs > li > a, .pills > li > a {
+  display: block;
+}
+.tabs {
+  border-color: #ddd;
+  border-style: solid;
+  border-width: 0 0 1px;
+}
+.tabs > li {
+  position: relative;
+  margin-bottom: -1px;
+}
+.tabs > li > a {
+  padding: 0 15px;
+  margin-right: 2px;
+  line-height: 34px;
+  border: 1px solid transparent;
+  -webkit-border-radius: 4px 4px 0 0;
+  -moz-border-radius: 4px 4px 0 0;
+  border-radius: 4px 4px 0 0;
+}
+.tabs > li > a:hover {
+  text-decoration: none;
+  background-color: #eee;
+  border-color: #eee #eee #ddd;
+}
+.tabs .active > a, .tabs .active > a:hover {
+  color: #808080;
+  background-color: #ffffff;
+  border: 1px solid #ddd;
+  border-bottom-color: transparent;
+  cursor: default;
+}
+.tabs .menu-dropdown, .tabs .dropdown-menu {
+  top: 35px;
+  border-width: 1px;
+  -webkit-border-radius: 0 6px 6px 6px;
+  -moz-border-radius: 0 6px 6px 6px;
+  border-radius: 0 6px 6px 6px;
+}
+.tabs a.menu:after, .tabs .dropdown-toggle:after {
+  border-top-color: #999;
+  margin-top: 15px;
+  margin-left: 5px;
+}
+.tabs li.open.menu .menu, .tabs .open.dropdown .dropdown-toggle {
+  border-color: #999;
+}
+.tabs li.open a.menu:after, .tabs .dropdown.open .dropdown-toggle:after {
+  border-top-color: #555;
+}
+.pills a {
+  margin: 5px 3px 5px 0;
+  padding: 0 15px;
+  line-height: 30px;
+  text-shadow: 0 1px 1px #ffffff;
+  -webkit-border-radius: 15px;
+  -moz-border-radius: 15px;
+  border-radius: 15px;
+}
+.pills a:hover {
+  color: #ffffff;
+  text-decoration: none;
+  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.25);
+  background-color: #00438a;
+}
+.pills .active a {
+  color: #ffffff;
+  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.25);
+  background-color: #0069d6;
+}
+.pills-vertical > li {
+  float: none;
+}
+.tab-content > .tab-pane,
+.pill-content > .pill-pane,
+.tab-content > div,
+.pill-content > div {
+  display: none;
+}
+.tab-content > .active, .pill-content > .active {
+  display: block;
+}
+.breadcrumb {
+  padding: 7px 14px;
+  margin: 0 0 18px;
+  background-color: #f5f5f5;
+  background-repeat: repeat-x;
+  background-image: -khtml-gradient(linear, left top, left bottom, from(#ffffff), to(#f5f5f5));
+  background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5);
+  background-image: -ms-linear-gradient(top, #ffffff, #f5f5f5);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f5f5f5));
+  background-image: -webkit-linear-gradient(top, #ffffff, #f5f5f5);
+  background-image: -o-linear-gradient(top, #ffffff, #f5f5f5);
+  background-image: linear-gradient(top, #ffffff, #f5f5f5);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0);
+  border: 1px solid #ddd;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+  -webkit-box-shadow: inset 0 1px 0 #ffffff;
+  -moz-box-shadow: inset 0 1px 0 #ffffff;
+  box-shadow: inset 0 1px 0 #ffffff;
+}
+.breadcrumb li {
+  display: inline;
+  text-shadow: 0 1px 0 #ffffff;
+}
+.breadcrumb .divider {
+  padding: 0 5px;
+  color: #bfbfbf;
+}
+.breadcrumb .active a {
+  color: #404040;
+}
+.hero-unit {
+  background-color: #f5f5f5;
+  margin-bottom: 30px;
+  padding: 60px;
+  -webkit-border-radius: 6px;
+  -moz-border-radius: 6px;
+  border-radius: 6px;
+}
+.hero-unit h1 {
+  margin-bottom: 0;
+  font-size: 60px;
+  line-height: 1;
+  letter-spacing: -1px;
+}
+.hero-unit p {
+  font-size: 18px;
+  font-weight: 200;
+  line-height: 27px;
+}
+footer {
+  margin-top: 17px;
+  padding-top: 17px;
+  border-top: 1px solid #eee;
+}
+.page-header {
+  margin-bottom: 17px;
+  border-bottom: 1px solid #ddd;
+  -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+  -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+}
+.page-header h1 {
+  margin-bottom: 8px;
+}
+.btn.danger,
+.alert-message.danger,
+.btn.danger:hover,
+.alert-message.danger:hover,
+.btn.error,
+.alert-message.error,
+.btn.error:hover,
+.alert-message.error:hover,
+.btn.success,
+.alert-message.success,
+.btn.success:hover,
+.alert-message.success:hover,
+.btn.info,
+.alert-message.info,
+.btn.info:hover,
+.alert-message.info:hover {
+  color: #ffffff;
+}
+.btn .close, .alert-message .close {
+  font-family: Arial, sans-serif;
+  line-height: 18px;
+}
+.btn.danger,
+.alert-message.danger,
+.btn.error,
+.alert-message.error {
+  background-color: #c43c35;
+  background-repeat: repeat-x;
+  background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35));
+  background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
+  background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35));
+  background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
+  background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
+  background-image: linear-gradient(top, #ee5f5b, #c43c35);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+  border-color: #c43c35 #c43c35 #882a25;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+}
+.btn.success, .alert-message.success {
+  background-color: #57a957;
+  background-repeat: repeat-x;
+  background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));
+  background-image: -moz-linear-gradient(top, #62c462, #57a957);
+  background-image: -ms-linear-gradient(top, #62c462, #57a957);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957));
+  background-image: -webkit-linear-gradient(top, #62c462, #57a957);
+  background-image: -o-linear-gradient(top, #62c462, #57a957);
+  background-image: linear-gradient(top, #62c462, #57a957);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+  border-color: #57a957 #57a957 #3d773d;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+}
+.btn.info, .alert-message.info {
+  background-color: #339bb9;
+  background-repeat: repeat-x;
+  background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9));
+  background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
+  background-image: -ms-linear-gradient(top, #5bc0de, #339bb9);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9));
+  background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
+  background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
+  background-image: linear-gradient(top, #5bc0de, #339bb9);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+  border-color: #339bb9 #339bb9 #22697d;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+}
+.btn {
+  cursor: pointer;
+  display: inline-block;
+  background-color: #e6e6e6;
+  background-repeat: no-repeat;
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6));
+  background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
+  background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6);
+  background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
+  background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
+  background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);
+  padding: 5px 14px 6px;
+  text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
+  color: #333;
+  font-size: 13px;
+  line-height: normal;
+  border: 1px solid #ccc;
+  border-bottom-color: #bbb;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+  -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+  -webkit-transition: 0.1s linear all;
+  -moz-transition: 0.1s linear all;
+  -ms-transition: 0.1s linear all;
+  -o-transition: 0.1s linear all;
+  transition: 0.1s linear all;
+}
+.btn:hover {
+  background-position: 0 -15px;
+  color: #333;
+  text-decoration: none;
+}
+.btn:focus {
+  outline: 1px dotted #666;
+}
+.btn.primary {
+  color: #ffffff;
+  background-color: #0064cd;
+  background-repeat: repeat-x;
+  background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));
+  background-image: -moz-linear-gradient(top, #049cdb, #0064cd);
+  background-image: -ms-linear-gradient(top, #049cdb, #0064cd);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));
+  background-image: -webkit-linear-gradient(top, #049cdb, #0064cd);
+  background-image: -o-linear-gradient(top, #049cdb, #0064cd);
+  background-image: linear-gradient(top, #049cdb, #0064cd);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#049cdb', endColorstr='#0064cd', GradientType=0);
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+  border-color: #0064cd #0064cd #003f81;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+}
+.btn.active, .btn:active {
+  -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
+  -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
+  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+.btn.disabled {
+  cursor: default;
+  background-image: none;
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  filter: alpha(opacity=65);
+  -khtml-opacity: 0.65;
+  -moz-opacity: 0.65;
+  opacity: 0.65;
+  -webkit-box-shadow: none;
+  -moz-box-shadow: none;
+  box-shadow: none;
+}
+.btn[disabled] {
+  cursor: default;
+  background-image: none;
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  filter: alpha(opacity=65);
+  -khtml-opacity: 0.65;
+  -moz-opacity: 0.65;
+  opacity: 0.65;
+  -webkit-box-shadow: none;
+  -moz-box-shadow: none;
+  box-shadow: none;
+}
+.btn.large {
+  font-size: 15px;
+  line-height: normal;
+  padding: 9px 14px 9px;
+  -webkit-border-radius: 6px;
+  -moz-border-radius: 6px;
+  border-radius: 6px;
+}
+.btn.small {
+  padding: 7px 9px 7px;
+  font-size: 11px;
+}
+:root .alert-message, :root .btn {
+  border-radius: 0 \0;
+}
+button.btn::-moz-focus-inner, input[type=submit].btn::-moz-focus-inner {
+  padding: 0;
+  border: 0;
+}
+.close {
+  float: right;
+  color: #000000;
+  font-size: 20px;
+  font-weight: bold;
+  line-height: 13.5px;
+  text-shadow: 0 1px 0 #ffffff;
+  filter: alpha(opacity=25);
+  -khtml-opacity: 0.25;
+  -moz-opacity: 0.25;
+  opacity: 0.25;
+}
+.close:hover {
+  color: #000000;
+  text-decoration: none;
+  filter: alpha(opacity=40);
+  -khtml-opacity: 0.4;
+  -moz-opacity: 0.4;
+  opacity: 0.4;
+}
+.alert-message {
+  position: relative;
+  padding: 7px 15px;
+  margin-bottom: 18px;
+  color: #404040;
+  background-color: #eedc94;
+  background-repeat: repeat-x;
+  background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94));
+  background-image: -moz-linear-gradient(top, #fceec1, #eedc94);
+  background-image: -ms-linear-gradient(top, #fceec1, #eedc94);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94));
+  background-image: -webkit-linear-gradient(top, #fceec1, #eedc94);
+  background-image: -o-linear-gradient(top, #fceec1, #eedc94);
+  background-image: linear-gradient(top, #fceec1, #eedc94);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0);
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+  border-color: #eedc94 #eedc94 #e4c652;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+  border-width: 1px;
+  border-style: solid;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
+  -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
+}
+.alert-message .close {
+  margin-top: 1px;
+  *margin-top: 0;
+}
+.alert-message a {
+  font-weight: bold;
+  color: #404040;
+}
+.alert-message.danger p a,
+.alert-message.error p a,
+.alert-message.success p a,
+.alert-message.info p a {
+  color: #ffffff;
+}
+.alert-message h5 {
+  line-height: 18px;
+}
+.alert-message p {
+  margin-bottom: 0;
+}
+.alert-message div {
+  margin-top: 5px;
+  margin-bottom: 2px;
+  line-height: 28px;
+}
+.alert-message .btn {
+  -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
+  -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
+  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
+}
+.alert-message.block-message {
+  background-image: none;
+  background-color: #fdf5d9;
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  padding: 14px;
+  border-color: #fceec1;
+  -webkit-box-shadow: none;
+  -moz-box-shadow: none;
+  box-shadow: none;
+}
+.alert-message.block-message ul, .alert-message.block-message p {
+  margin-right: 30px;
+}
+.alert-message.block-message ul {
+  margin-bottom: 0;
+}
+.alert-message.block-message li {
+  color: #404040;
+}
+.alert-message.block-message .alert-actions {
+  margin-top: 5px;
+}
+.alert-message.block-message.error, .alert-message.block-message.success, .alert-message.block-message.info {
+  color: #404040;
+  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+}
+.alert-message.block-message.error {
+  background-color: #fddfde;
+  border-color: #fbc7c6;
+}
+.alert-message.block-message.success {
+  background-color: #d1eed1;
+  border-color: #bfe7bf;
+}
+.alert-message.block-message.info {
+  background-color: #ddf4fb;
+  border-color: #c6edf9;
+}
+.alert-message.block-message.danger p a,
+.alert-message.block-message.error p a,
+.alert-message.block-message.success p a,
+.alert-message.block-message.info p a {
+  color: #404040;
+}
+.pagination {
+  height: 36px;
+  margin: 18px 0;
+}
+.pagination ul {
+  float: left;
+  margin: 0;
+  border: 1px solid #ddd;
+  border: 1px solid rgba(0, 0, 0, 0.15);
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+  -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+.pagination li {
+  display: inline;
+}
+.pagination a {
+  float: left;
+  padding: 0 14px;
+  line-height: 34px;
+  border-right: 1px solid;
+  border-right-color: #ddd;
+  border-right-color: rgba(0, 0, 0, 0.15);
+  *border-right-color: #ddd;
+  /* IE6-7 */
+
+  text-decoration: none;
+}
+.pagination a:hover, .pagination .active a {
+  background-color: #c7eefe;
+}
+.pagination .disabled a, .pagination .disabled a:hover {
+  background-color: transparent;
+  color: #bfbfbf;
+}
+.pagination .next a {
+  border: 0;
+}
+.well {
+  background-color: #f5f5f5;
+  margin-bottom: 20px;
+  padding: 19px;
+  min-height: 20px;
+  border: 1px solid #eee;
+  border: 1px solid rgba(0, 0, 0, 0.05);
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+  -moz-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);
+}
+.modal-backdrop {
+  background-color: #000000;
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 10000;
+}
+.modal-backdrop.fade {
+  opacity: 0;
+}
+.modal-backdrop, .modal-backdrop.fade.in {
+  filter: alpha(opacity=80);
+  -khtml-opacity: 0.8;
+  -moz-opacity: 0.8;
+  opacity: 0.8;
+}
+.modal {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  z-index: 11000;
+  width: 560px;
+  margin: -250px 0 0 -280px;
+  background-color: #ffffff;
+  border: 1px solid #999;
+  border: 1px solid rgba(0, 0, 0, 0.3);
+  *border: 1px solid #999;
+  /* IE6-7 */
+
+  -webkit-border-radius: 6px;
+  -moz-border-radius: 6px;
+  border-radius: 6px;
+  -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
+  -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
+  box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
+  -webkit-background-clip: padding-box;
+  -moz-background-clip: padding-box;
+  background-clip: padding-box;
+}
+.modal .close {
+  margin-top: 7px;
+}
+.modal.fade {
+  -webkit-transition: opacity .3s linear, top .3s ease-out;
+  -moz-transition: opacity .3s linear, top .3s ease-out;
+  -ms-transition: opacity .3s linear, top .3s ease-out;
+  -o-transition: opacity .3s linear, top .3s ease-out;
+  transition: opacity .3s linear, top .3s ease-out;
+  top: -25%;
+}
+.modal.fade.in {
+  top: 50%;
+}
+.modal-header {
+  border-bottom: 1px solid #eee;
+  padding: 5px 15px;
+}
+.modal-body {
+  padding: 15px;
+}
+.modal-body form {
+  margin-bottom: 0;
+}
+.modal-footer {
+  background-color: #f5f5f5;
+  padding: 14px 15px 15px;
+  border-top: 1px solid #ddd;
+  -webkit-border-radius: 0 0 6px 6px;
+  -moz-border-radius: 0 0 6px 6px;
+  border-radius: 0 0 6px 6px;
+  -webkit-box-shadow: inset 0 1px 0 #ffffff;
+  -moz-box-shadow: inset 0 1px 0 #ffffff;
+  box-shadow: inset 0 1px 0 #ffffff;
+  zoom: 1;
+  margin-bottom: 0;
+}
+.modal-footer:before, .modal-footer:after {
+  display: table;
+  content: "";
+  zoom: 1;
+}
+.modal-footer:after {
+  clear: both;
+}
+.modal-footer .btn {
+  float: right;
+  margin-left: 5px;
+}
+.modal .popover, .modal .twipsy {
+  z-index: 12000;
+}
+.twipsy {
+  display: block;
+  position: absolute;
+  visibility: visible;
+  padding: 5px;
+  font-size: 11px;
+  z-index: 1000;
+  filter: alpha(opacity=80);
+  -khtml-opacity: 0.8;
+  -moz-opacity: 0.8;
+  opacity: 0.8;
+}
+.twipsy.fade.in {
+  filter: alpha(opacity=80);
+  -khtml-opacity: 0.8;
+  -moz-opacity: 0.8;
+  opacity: 0.8;
+}
+.twipsy.above .twipsy-arrow {
+  bottom: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-left: 5px solid transparent;
+  border-right: 5px solid transparent;
+  border-top: 5px solid #000000;
+}
+.twipsy.left .twipsy-arrow {
+  top: 50%;
+  right: 0;
+  margin-top: -5px;
+  border-top: 5px solid transparent;
+  border-bottom: 5px solid transparent;
+  border-left: 5px solid #000000;
+}
+.twipsy.below .twipsy-arrow {
+  top: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-left: 5px solid transparent;
+  border-right: 5px solid transparent;
+  border-bottom: 5px solid #000000;
+}
+.twipsy.right .twipsy-arrow {
+  top: 50%;
+  left: 0;
+  margin-top: -5px;
+  border-top: 5px solid transparent;
+  border-bottom: 5px solid transparent;
+  border-right: 5px solid #000000;
+}
+.twipsy-inner {
+  padding: 3px 8px;
+  background-color: #000000;
+  color: white;
+  text-align: center;
+  max-width: 200px;
+  text-decoration: none;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.twipsy-arrow {
+  position: absolute;
+  width: 0;
+  height: 0;
+}
+.popover {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 1000;
+  padding: 5px;
+  display: none;
+}
+.popover.above .arrow {
+  bottom: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-left: 5px solid transparent;
+  border-right: 5px solid transparent;
+  border-top: 5px solid #000000;
+}
+.popover.right .arrow {
+  top: 50%;
+  left: 0;
+  margin-top: -5px;
+  border-top: 5px solid transparent;
+  border-bottom: 5px solid transparent;
+  border-right: 5px solid #000000;
+}
+.popover.below .arrow {
+  top: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-left: 5px solid transparent;
+  border-right: 5px solid transparent;
+  border-bottom: 5px solid #000000;
+}
+.popover.left .arrow {
+  top: 50%;
+  right: 0;
+  margin-top: -5px;
+  border-top: 5px solid transparent;
+  border-bottom: 5px solid transparent;
+  border-left: 5px solid #000000;
+}
+.popover .arrow {
+  position: absolute;
+  width: 0;
+  height: 0;
+}
+.popover .inner {
+  background: #000000;
+  background: rgba(0, 0, 0, 0.8);
+  padding: 3px;
+  overflow: hidden;
+  width: 280px;
+  -webkit-border-radius: 6px;
+  -moz-border-radius: 6px;
+  border-radius: 6px;
+  -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
+  -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
+  box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
+}
+.popover .title {
+  background-color: #f5f5f5;
+  padding: 9px 15px;
+  line-height: 1;
+  -webkit-border-radius: 3px 3px 0 0;
+  -moz-border-radius: 3px 3px 0 0;
+  border-radius: 3px 3px 0 0;
+  border-bottom: 1px solid #eee;
+}
+.popover .content {
+  background-color: #ffffff;
+  padding: 14px;
+  -webkit-border-radius: 0 0 3px 3px;
+  -moz-border-radius: 0 0 3px 3px;
+  border-radius: 0 0 3px 3px;
+  -webkit-background-clip: padding-box;
+  -moz-background-clip: padding-box;
+  background-clip: padding-box;
+}
+.popover .content p, .popover .content ul, .popover .content ol {
+  margin-bottom: 0;
+}
+.fade {
+  -webkit-transition: opacity 0.15s linear;
+  -moz-transition: opacity 0.15s linear;
+  -ms-transition: opacity 0.15s linear;
+  -o-transition: opacity 0.15s linear;
+  transition: opacity 0.15s linear;
+  opacity: 0;
+}
+.fade.in {
+  opacity: 1;
+}
+.label {
+  padding: 1px 3px 2px;
+  font-size: 9.75px;
+  font-weight: bold;
+  color: #ffffff;
+  text-transform: uppercase;
+  white-space: nowrap;
+  background-color: #bfbfbf;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+}
+.label.important {
+  background-color: #c43c35;
+}
+.label.warning {
+  background-color: #f89406;
+}
+.label.success {
+  background-color: #46a546;
+}
+.label.notice {
+  background-color: #62cffc;
+}
+.media-grid {
+  margin-left: -20px;
+  margin-bottom: 0;
+  zoom: 1;
+}
+.media-grid:before, .media-grid:after {
+  display: table;
+  content: "";
+  zoom: 1;
+}
+.media-grid:after {
+  clear: both;
+}
+.media-grid li {
+  display: inline;
+}
+.media-grid a {
+  float: left;
+  padding: 4px;
+  margin: 0 0 18px 20px;
+  border: 1px solid #ddd;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075);
+  -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075);
+  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075);
+}
+.media-grid a img {
+  display: block;
+}
+.media-grid a:hover {
+  border-color: #0069d6;
+  -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25);
+  -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25);
+  box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25);
+}
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/bootstrap-dropdown-1.4.0.js
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/bootstrap-dropdown-1.4.0.js	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/bootstrap-dropdown-1.4.0.js	(revision 10886)
@@ -0,0 +1,55 @@
+/* ============================================================
+ * bootstrap-dropdown.js v1.4.0
+ * http://twitter.github.com/bootstrap/javascript.html#dropdown
+ * ============================================================
+ * Copyright 2011 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 PLUGIN DEFINITION
+   * ========================== */
+
+  $.fn.dropdown = function ( selector ) {
+    return this.each(function () {
+      $(this).delegate(selector || d, 'click', function (e) {
+        var li = $(this).parent('li')
+          , isActive = li.hasClass('open')
+
+        clearMenus()
+        !isActive && li.toggleClass('open')
+        return false
+      })
+    })
+  }
+
+  /* APPLY TO STANDARD DROPDOWN ELEMENTS
+   * =================================== */
+
+  var d = 'a.menu, .dropdown-toggle'
+
+  function clearMenus() {
+    $(d).parent('li').removeClass('open')
+  }
+
+  $(function () {
+    $('html').bind("click", clearMenus)
+    $('body').dropdown( '[data-dropdown] a.menu, [data-dropdown] .dropdown-toggle' )
+  })
+
+}( window.jQuery || window.ender );
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/bootstrap-tabs-1.4.0.js
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/bootstrap-tabs-1.4.0.js	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/bootstrap-tabs-1.4.0.js	(revision 10886)
@@ -0,0 +1,80 @@
+/* ========================================================
+ * bootstrap-tabs.js v1.4.0
+ * http://twitter.github.com/bootstrap/javascript.html#tabs
+ * ========================================================
+ * Copyright 2011 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"
+
+  function activate ( element, container ) {
+    container
+      .find('> .active')
+      .removeClass('active')
+      .find('> .dropdown-menu > .active')
+      .removeClass('active')
+
+    element.addClass('active')
+
+    if ( element.parent('.dropdown-menu') ) {
+      element.closest('li.dropdown').addClass('active')
+    }
+  }
+
+  function tab( e ) {
+    var $this = $(this)
+      , $ul = $this.closest('ul:not(.dropdown-menu)')
+      , href = $this.attr('href')
+      , previous
+      , $href
+
+    if ( /^#\w+/.test(href) ) {
+      e.preventDefault()
+
+      if ( $this.parent('li').hasClass('active') ) {
+        return
+      }
+
+      previous = $ul.find('.active a').last()[0]
+      $href = $(href)
+
+      activate($this.parent('li'), $ul)
+      activate($href, $href.parent())
+
+      $this.trigger({
+        type: 'change'
+      , relatedTarget: previous
+      })
+    }
+  }
+
+
+ /* TABS/PILLS PLUGIN DEFINITION
+  * ============================ */
+
+  $.fn.tabs = $.fn.pills = function ( selector ) {
+    return this.each(function () {
+      $(this).delegate(selector || '.tabs li > a, .pills > li > a', 'click', tab)
+    })
+  }
+
+  $(document).ready(function () {
+    $('body').tabs('ul[data-tabs] li > a, ul[data-pills] > li > a')
+  })
+
+}( window.jQuery || window.ender );
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/datatable.js
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/datatable.js	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/datatable.js	(revision 10886)
@@ -0,0 +1,20 @@
+$(document).ready(function(){
+  $('.dataTable').dataTable();
+
+	$('.dataTableFiles').dataTable( {
+		"aaSorting": [[ 2, "desc" ]],
+	} );
+
+	$('.dataTableManage').dataTable( {
+		"aaSorting": [[ 1, "asc" ]],
+		"aoColumnDefs":[{ "bSortable": false, "aTargets": [ 0 ] }]
+	} );
+
+	$('.dataTableManageFiles').dataTable( {
+		"aaSorting": [[ 3, "desc" ]],
+		"aoColumnDefs":[{ "bSortable": false, "aTargets": [ 0 ] }]
+	} );
+
+
+});
+
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/datatables.css
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/datatables.css	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/datatables.css	(revision 10886)
@@ -0,0 +1,521 @@
+/*
+ *  File:         demo_table_jui.css
+ *  CVS:          $Id$
+ *  Description:  CSS descriptions for DataTables demo pages
+ *  Author:       Allan Jardine
+ *  Created:      Tue May 12 06:47:22 BST 2009
+ *  Modified:     $Date$ by $Author$
+ *  Language:     CSS
+ *  Project:      DataTables
+ *
+ *  Copyright 2009 Allan Jardine. All Rights Reserved.
+ *
+ * ***************************************************************************
+ * DESCRIPTION
+ *
+ * The styles given here are suitable for the demos that are used with the standard DataTables
+ * distribution (see www.datatables.net). You will most likely wish to modify these styles to
+ * meet the layout requirements of your site.
+ *
+ * Common issues:
+ *   'full_numbers' pagination - I use an extra selector on the body tag to ensure that there is
+ *     no conflict between the two pagination types. If you want to use full_numbers pagination
+ *     ensure that you either have "example_alt_pagination" as a body class name, or better yet,
+ *     modify that selector.
+ *   Note that the path used for Images is relative. All images are by default located in
+ *     ../images/ - relative to this CSS file.
+ */
+
+
+/*
+ * jQuery UI specific styling
+ */
+
+.paging_two_button .ui-button {
+	float: left;
+	cursor: pointer;
+	* cursor: hand;
+}
+
+.paging_full_numbers .ui-button {
+	padding: 2px 6px;
+	margin: 0;
+	cursor: pointer;
+	* cursor: hand;
+}
+
+.dataTables_paginate .ui-button {
+	margin-right: -0.1em !important;
+}
+
+.paging_full_numbers {
+	width: 350px !important;
+}
+
+.dataTables_wrapper .ui-toolbar {
+	padding: 5px;
+}
+
+.dataTables_paginate {
+	width: auto;
+}
+
+.dataTables_info {
+	padding-top: 3px;
+}
+
+table.display thead th {
+	padding: 3px 0px 3px 10px;
+	cursor: pointer;
+	* cursor: hand;
+}
+
+div.dataTables_wrapper .ui-widget-header {
+	font-weight: normal;
+}
+
+
+/*
+ * Sort arrow icon positioning
+ */
+table.display thead th div.DataTables_sort_wrapper {
+	position: relative;
+	padding-right: 20px;
+	padding-right: 20px;
+}
+
+table.display thead th div.DataTables_sort_wrapper span {
+	position: absolute;
+	top: 50%;
+	margin-top: -8px;
+	right: 0;
+}
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ *
+ * Everything below this line is the same as demo_table.css. This file is
+ * required for 'cleanliness' of the markup
+ *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables features
+ */
+
+.dataTables_wrapper {
+	position: relative;
+	min-height: 302px;
+	_height: 302px;
+	clear: both;
+}
+
+.dataTables_processing {
+	position: absolute;
+	top: 0px;
+	left: 50%;
+	width: 250px;
+	margin-left: -125px;
+	border: 1px solid #ddd;
+	text-align: center;
+	color: #999;
+	font-size: 11px;
+	padding: 2px 0;
+}
+
+.dataTables_length {
+	width: 40%;
+	float: left;
+}
+
+.dataTables_filter {
+	width: 50%;
+	float: right;
+	text-align: right;
+}
+
+.dataTables_info {
+	width: 50%;
+	float: left;
+}
+
+.dataTables_paginate {
+	float: right;
+	text-align: right;
+}
+
+/* Pagination nested */
+.paginate_disabled_previous, .paginate_enabled_previous, .paginate_disabled_next, .paginate_enabled_next {
+	height: 19px;
+	width: 19px;
+	margin-left: 3px;
+	float: left;
+}
+
+.paginate_disabled_previous {
+	background-image: url('back_disabled.jpg');
+}
+
+.paginate_enabled_previous {
+	background-image: url('back_enabled.jpg');
+}
+
+.paginate_disabled_next {
+	background-image: url('forward_disabled.jpg');
+}
+
+.paginate_enabled_next {
+	background-image: url('forward_enabled.jpg');
+}
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables display
+ */
+table.display {
+	margin: 0 auto;
+	width: 100%;
+	clear: both;
+	border-collapse: collapse;
+}
+
+table.display tfoot th {
+	padding: 3px 0px 3px 10px;
+	font-weight: bold;
+	font-weight: normal;
+}
+
+table.display tr.heading2 td {
+	border-bottom: 1px solid #aaa;
+}
+
+table.display td {
+	padding: 3px 10px;
+}
+
+table.display td.center {
+	text-align: center;
+}
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables sorting
+ */
+
+.sorting_asc {
+	background: url('sort_asc.png') no-repeat center right;
+}
+
+.sorting_desc {
+	background: url('sort_desc.png') no-repeat center right;
+}
+
+.sorting {
+	background: url('sort_both.png') no-repeat center right;
+}
+
+.sorting_asc_disabled {
+	background: url('sort_asc_disabled.png') no-repeat center right;
+}
+
+.sorting_desc_disabled {
+	background: url('sort_desc_disabled.png') no-repeat center right;
+}
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * DataTables row classes
+ */
+table.display tr.odd.gradeA {
+	background-color: #ddffdd;
+}
+
+table.display tr.even.gradeA {
+	background-color: #eeffee;
+}
+
+
+
+
+table.display tr.odd.gradeA {
+	background-color: #ddffdd;
+}
+
+table.display tr.even.gradeA {
+	background-color: #eeffee;
+}
+
+table.display tr.odd.gradeC {
+	background-color: #ddddff;
+}
+
+table.display tr.even.gradeC {
+	background-color: #eeeeff;
+}
+
+table.display tr.odd.gradeX {
+	background-color: #ffdddd;
+}
+
+table.display tr.even.gradeX {
+	background-color: #ffeeee;
+}
+
+table.display tr.odd.gradeU {
+	background-color: #ddd;
+}
+
+table.display tr.even.gradeU {
+	background-color: #eee;
+}
+
+
+tr.odd {
+	background-color: #E2E4FF;
+}
+
+tr.even {
+	background-color: white;
+}
+
+
+
+
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Misc
+ */
+.dataTables_scroll {
+	clear: both;
+}
+
+.top, .bottom {
+	padding: 15px;
+	background-color: #F5F5F5;
+	border: 1px solid #CCCCCC;
+}
+
+.top .dataTables_info {
+	float: none;
+}
+
+.clear {
+	clear: both;
+}
+
+.dataTables_empty {
+	text-align: center;
+}
+
+tfoot input {
+	margin: 0.5em 0;
+	width: 100%;
+	color: #444;
+}
+
+tfoot input.search_init {
+	color: #999;
+}
+
+td.group {
+	background-color: #d1cfd0;
+	border-bottom: 2px solid #A19B9E;
+	border-top: 2px solid #A19B9E;
+}
+
+td.details {
+	background-color: #d1cfd0;
+	border: 2px solid #A19B9E;
+}
+
+
+.example_alt_pagination div.dataTables_info {
+	width: 40%;
+}
+
+.paging_full_numbers span.paginate_button,
+ 	.paging_full_numbers span.paginate_active {
+	border: 1px solid #aaa;
+	-webkit-border-radius: 5px;
+	-moz-border-radius: 5px;
+	padding: 2px 5px;
+	margin: 0 3px;
+	cursor: pointer;
+	*cursor: hand;
+}
+
+.paging_full_numbers span.paginate_button {
+	background-color: #ddd;
+}
+
+.paging_full_numbers span.paginate_button:hover {
+	background-color: #ccc;
+}
+
+.paging_full_numbers span.paginate_active {
+	background-color: #99B3FF;
+}
+
+table.display tr.even.row_selected td {
+	background-color: #B0BED9;
+}
+
+table.display tr.odd.row_selected td {
+	background-color: #9FAFD1;
+}
+
+
+/*
+ * Sorting classes for columns
+ */
+/* For the standard odd/even */
+tr.odd td.sorting_1 {
+	background-color: #D3D6FF;
+}
+
+tr.odd td.sorting_2 {
+	background-color: #DADCFF;
+}
+
+tr.odd td.sorting_3 {
+	background-color: #E0E2FF;
+}
+
+tr.even td.sorting_1 {
+	background-color: #EAEBFF;
+}
+
+tr.even td.sorting_2 {
+	background-color: #F2F3FF;
+}
+
+tr.even td.sorting_3 {
+	background-color: #F9F9FF;
+}
+
+
+/* For the Conditional-CSS grading rows */
+/*
+ 	Colour calculations (based off the main row colours)
+  Level 1:
+		dd > c4
+		ee > d5
+	Level 2:
+	  dd > d1
+	  ee > e2
+ */
+tr.odd.gradeA td.sorting_1 {
+	background-color: #c4ffc4;
+}
+
+tr.odd.gradeA td.sorting_2 {
+	background-color: #d1ffd1;
+}
+
+tr.odd.gradeA td.sorting_3 {
+	background-color: #d1ffd1;
+}
+
+tr.even.gradeA td.sorting_1 {
+	background-color: #d5ffd5;
+}
+
+tr.even.gradeA td.sorting_2 {
+	background-color: #e2ffe2;
+}
+
+tr.even.gradeA td.sorting_3 {
+	background-color: #e2ffe2;
+}
+
+tr.odd.gradeC td.sorting_1 {
+	background-color: #c4c4ff;
+}
+
+tr.odd.gradeC td.sorting_2 {
+	background-color: #d1d1ff;
+}
+
+tr.odd.gradeC td.sorting_3 {
+	background-color: #d1d1ff;
+}
+
+tr.even.gradeC td.sorting_1 {
+	background-color: #d5d5ff;
+}
+
+tr.even.gradeC td.sorting_2 {
+	background-color: #e2e2ff;
+}
+
+tr.even.gradeC td.sorting_3 {
+	background-color: #e2e2ff;
+}
+
+tr.odd.gradeX td.sorting_1 {
+	background-color: #ffc4c4;
+}
+
+tr.odd.gradeX td.sorting_2 {
+	background-color: #ffd1d1;
+}
+
+tr.odd.gradeX td.sorting_3 {
+	background-color: #ffd1d1;
+}
+
+tr.even.gradeX td.sorting_1 {
+	background-color: #ffd5d5;
+}
+
+tr.even.gradeX td.sorting_2 {
+	background-color: #ffe2e2;
+}
+
+tr.even.gradeX td.sorting_3 {
+	background-color: #ffe2e2;
+}
+
+tr.odd.gradeU td.sorting_1 {
+	background-color: #c4c4c4;
+}
+
+tr.odd.gradeU td.sorting_2 {
+	background-color: #d1d1d1;
+}
+
+tr.odd.gradeU td.sorting_3 {
+	background-color: #d1d1d1;
+}
+
+tr.even.gradeU td.sorting_1 {
+	background-color: #d5d5d5;
+}
+
+tr.even.gradeU td.sorting_2 {
+	background-color: #e2e2e2;
+}
+
+tr.even.gradeU td.sorting_3 {
+	background-color: #e2e2e2;
+}
+
+
+/*
+ * Row highlighting example
+ */
+.ex_highlight #example tbody tr.even:hover, #example tbody tr.even td.highlighted {
+	background-color: #ECFFB3;
+}
+
+.ex_highlight #example tbody tr.odd:hover, #example tbody tr.odd td.highlighted {
+	background-color: #E6FF99;
+}
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/datepicker.js
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/datepicker.js	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/datepicker.js	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/static/empty.css
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/empty.css	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/empty.css	(revision 10886)
@@ -0,0 +1,1 @@
+/* Nothing here */
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/jquery.dataTables.js
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/jquery.dataTables.js	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/jquery.dataTables.js	(revision 10886)
@@ -0,0 +1,6862 @@
+/*
+ * File:        jquery.dataTables.js
+ * Version:     1.7.6
+ * Description: Paginate, search and sort HTML tables
+ * Author:      Allan Jardine (www.sprymedia.co.uk)
+ * Created:     28/3/2008
+ * Language:    Javascript
+ * License:     GPL v2 or BSD 3 point style
+ * Project:     Mtaala
+ * Contact:     allan.jardine@sprymedia.co.uk
+ * 
+ * Copyright 2008-2010 Allan Jardine, all rights reserved.
+ *
+ * This source file is free software, under either the GPL v2 license or a
+ * BSD style license, as supplied with this software.
+ * 
+ * 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
+ */
+
+/*
+ * When considering jsLint, we need to allow eval() as it it is used for reading cookies and 
+ * building the dynamic multi-column sort functions.
+ */
+/*jslint evil: true, undef: true, browser: true */
+/*globals $, jQuery,_fnExternApiFunc,_fnInitalise,_fnInitComplete,_fnLanguageProcess,_fnAddColumn,_fnColumnOptions,_fnAddData,_fnGatherData,_fnDrawHead,_fnDraw,_fnReDraw,_fnAjaxUpdate,_fnAjaxUpdateDraw,_fnAddOptionsHtml,_fnFeatureHtmlTable,_fnScrollDraw,_fnAjustColumnSizing,_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,_fnArrayCmp,_fnDetectType,_fnSettingsFromNode,_fnGetDataMaster,_fnGetTrNodes,_fnGetTdNodes,_fnEscapeRegex,_fnDeleteIndex,_fnReOrderIndex,_fnColumnOrdering,_fnLog,_fnClearTable,_fnSaveState,_fnLoadState,_fnCreateCookie,_fnReadCookie,_fnGetUniqueThs,_fnScrollBarWidth,_fnApplyToChildren,_fnMap*/
+
+(function($, window, document) {
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Section - DataTables variables
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+	
+	/*
+	 * Variable: dataTableSettings
+	 * Purpose:  Store the settings for each dataTables instance
+	 * Scope:    jQuery.fn
+	 */
+	$.fn.dataTableSettings = [];
+	var _aoSettings = $.fn.dataTableSettings; /* Short reference for fast internal lookup */
+	
+	/*
+	 * Variable: dataTableExt
+	 * Purpose:  Container for customisable parts of DataTables
+	 * Scope:    jQuery.fn
+	 */
+	$.fn.dataTableExt = {};
+	var _oExt = $.fn.dataTableExt;
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Section - DataTables extensible objects
+	 * 
+	 * The _oExt object is used to provide an area where user dfined plugins can be 
+	 * added to DataTables. The following properties of the object are used:
+	 *   oApi - Plug-in API functions
+	 *   aTypes - Auto-detection of types
+	 *   oSort - Sorting functions used by DataTables (based on the type)
+	 *   oPagination - Pagination functions for different input styles
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+	
+	/*
+	 * Variable: sVersion
+	 * Purpose:  Version string for plug-ins to check compatibility
+	 * Scope:    jQuery.fn.dataTableExt
+	 * Notes:    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
+	 */
+	_oExt.sVersion = "1.7.6";
+	
+	/*
+	 * Variable: sErrMode
+	 * Purpose:  How should DataTables report an error. Can take the value 'alert' or 'throw'
+	 * Scope:    jQuery.fn.dataTableExt
+	 */
+	_oExt.sErrMode = "alert";
+	
+	/*
+	 * Variable: iApiIndex
+	 * Purpose:  Index for what 'this' index API functions should use
+	 * Scope:    jQuery.fn.dataTableExt
+	 */
+	_oExt.iApiIndex = 0;
+	
+	/*
+	 * Variable: oApi
+	 * Purpose:  Container for plugin API functions
+	 * Scope:    jQuery.fn.dataTableExt
+	 */
+	_oExt.oApi = { };
+	
+	/*
+	 * Variable: aFiltering
+	 * Purpose:  Container for plugin filtering functions
+	 * Scope:    jQuery.fn.dataTableExt
+	 */
+	_oExt.afnFiltering = [ ];
+	
+	/*
+	 * Variable: aoFeatures
+	 * Purpose:  Container for plugin function functions
+	 * Scope:    jQuery.fn.dataTableExt
+	 * Notes:    Array of objects with the following parameters:
+	 *   fnInit: Function for initialisation of Feature. Takes oSettings and returns node
+	 *   cFeature: Character that will be matched in sDom - case sensitive
+	 *   sFeature: Feature name - just for completeness :-)
+	 */
+	_oExt.aoFeatures = [ ];
+	
+	/*
+	 * Variable: ofnSearch
+	 * Purpose:  Container for custom filtering functions
+	 * Scope:    jQuery.fn.dataTableExt
+	 * Notes:    This is an object (the name should match the type) for custom filtering function,
+	 *   which can be used for live DOM checking or formatted text filtering
+	 */
+	_oExt.ofnSearch = { };
+	
+	/*
+	 * Variable: afnSortData
+	 * Purpose:  Container for custom sorting data source functions
+	 * Scope:    jQuery.fn.dataTableExt
+	 * Notes:    Array (associative) of functions which is run prior to a column of this 
+	 *   'SortDataType' being sorted upon.
+	 *   Function input parameters:
+	 *     object:oSettings-  DataTables settings object
+	 *     int:iColumn - Target column number
+	 *   Return value: Array of data which exactly matched the full data set size for the column to
+	 *     be sorted upon
+	 */
+	_oExt.afnSortData = [ ];
+	
+	/*
+	 * Variable: oStdClasses
+	 * Purpose:  Storage for the various classes that DataTables uses
+	 * Scope:    jQuery.fn.dataTableExt
+	 */
+	_oExt.oStdClasses = {
+		/* 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",
+		"sPageFirst": "first",
+		"sPagePrevious": "previous",
+		"sPageNext": "next",
+		"sPageLast": "last",
+		
+		/* Stripping classes */
+		"sStripOdd": "odd",
+		"sStripEven": "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": "",
+		
+		/* Scrolling */
+		"sScrollWrapper": "dataTables_scroll",
+		"sScrollHead": "dataTables_scrollHead",
+		"sScrollHeadInner": "dataTables_scrollHeadInner",
+		"sScrollBody": "dataTables_scrollBody",
+		"sScrollFoot": "dataTables_scrollFoot",
+		"sScrollFootInner": "dataTables_scrollFootInner",
+		
+		/* Misc */
+		"sFooterTH": ""
+	};
+	
+	/*
+	 * Variable: oJUIClasses
+	 * Purpose:  Storage for the various classes that DataTables uses - jQuery UI suitable
+	 * Scope:    jQuery.fn.dataTableExt
+	 */
+	_oExt.oJUIClasses = {
+		/* 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",
+		"sPagePrevious": "previous",
+		"sPageNext": "next",
+		"sPageLast": "last ui-corner-tr ui-corner-br",
+		
+		/* Stripping classes */
+		"sStripOdd": "odd",
+		"sStripEven": "even",
+		
+		/* Empty row */
+		"sRowEmpty": "dataTables_empty",
+		
+		/* Features */
+		"sWrapper": "dataTables_wrapper",
+		"sFilter": "dataTables_filter",
+		"sInfo": "dataTables_info",
+		"sPaging": "dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi "+
+			"ui-buttonset-multi paging_", /* Note that the type is postfixed */
+		"sLength": "dataTables_length",
+		"sProcessing": "dataTables_processing",
+		
+		/* 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",
+		"sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */
+		"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",
+		
+		/* Scrolling */
+		"sScrollWrapper": "dataTables_scroll",
+		"sScrollHead": "dataTables_scrollHead ui-state-default",
+		"sScrollHeadInner": "dataTables_scrollHeadInner",
+		"sScrollBody": "dataTables_scrollBody",
+		"sScrollFoot": "dataTables_scrollFoot ui-state-default",
+		"sScrollFootInner": "dataTables_scrollFootInner",
+		
+		/* Misc */
+		"sFooterTH": "ui-state-default"
+	};
+	
+	/*
+	 * Variable: oPagination
+	 * Purpose:  Container for the various type of pagination that dataTables supports
+	 * Scope:    jQuery.fn.dataTableExt
+	 */
+	_oExt.oPagination = {
+		/*
+		 * Variable: two_button
+		 * Purpose:  Standard two button (forward/back) pagination
+	 	 * Scope:    jQuery.fn.dataTableExt.oPagination
+		 */
+		"two_button": {
+			/*
+			 * Function: oPagination.two_button.fnInit
+			 * Purpose:  Initalise 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 nPrevious, nNext, nPreviousInner, nNextInner;
+				
+				/* Store the next and previous elements in the oSettings object as they can be very
+				 * usful for automation - particularly testing
+				 */
+				if ( !oSettings.bJUI )
+				{
+					nPrevious = document.createElement( 'div' );
+					nNext = document.createElement( 'div' );
+				}
+				else
+				{
+					nPrevious = document.createElement( 'a' );
+					nNext = document.createElement( 'a' );
+					
+					nNextInner = document.createElement('span');
+					nNextInner.className = oSettings.oClasses.sPageJUINext;
+					nNext.appendChild( nNextInner );
+					
+					nPreviousInner = document.createElement('span');
+					nPreviousInner.className = oSettings.oClasses.sPageJUIPrev;
+					nPrevious.appendChild( nPreviousInner );
+				}
+				
+				nPrevious.className = oSettings.oClasses.sPagePrevDisabled;
+				nNext.className = oSettings.oClasses.sPageNextDisabled;
+				
+				nPrevious.title = oSettings.oLanguage.oPaginate.sPrevious;
+				nNext.title = oSettings.oLanguage.oPaginate.sNext;
+				
+				nPaging.appendChild( nPrevious );
+				nPaging.appendChild( nNext );
+				
+				$(nPrevious).bind( 'click.DT', function() {
+					if ( oSettings.oApi._fnPageChange( oSettings, "previous" ) )
+					{
+						/* Only draw when the page has actually changed */
+						fnCallbackDraw( oSettings );
+					}
+				} );
+				
+				$(nNext).bind( 'click.DT', function() {
+					if ( oSettings.oApi._fnPageChange( oSettings, "next" ) )
+					{
+						fnCallbackDraw( oSettings );
+					}
+				} );
+				
+				/* Take the brutal approach to cancelling text selection */
+				$(nPrevious).bind( 'selectstart.DT', function () { return false; } );
+				$(nNext).bind( 'selectstart.DT', function () { return false; } );
+				
+				/* ID the first elements only */
+				if ( oSettings.sTableId !== '' && typeof oSettings.aanFeatures.p == "undefined" )
+				{
+					nPaging.setAttribute( 'id', oSettings.sTableId+'_paginate' );
+					nPrevious.setAttribute( 'id', oSettings.sTableId+'_previous' );
+					nNext.setAttribute( 'id', oSettings.sTableId+'_next' );
+				}
+			},
+			
+			/*
+			 * 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;
+				}
+				
+				/* Loop over each instance of the pager */
+				var an = oSettings.aanFeatures.p;
+				for ( var i=0, iLen=an.length ; i<iLen ; i++ )
+				{
+					if ( an[i].childNodes.length !== 0 )
+					{
+						an[i].childNodes[0].className = 
+							( oSettings._iDisplayStart === 0 ) ? 
+							oSettings.oClasses.sPagePrevDisabled : oSettings.oClasses.sPagePrevEnabled;
+						
+						an[i].childNodes[1].className = 
+							( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() ) ? 
+							oSettings.oClasses.sPageNextDisabled : oSettings.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:  Initalise 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 nFirst = document.createElement( 'span' );
+				var nPrevious = document.createElement( 'span' );
+				var nList = document.createElement( 'span' );
+				var nNext = document.createElement( 'span' );
+				var nLast = document.createElement( 'span' );
+				
+				nFirst.innerHTML = oSettings.oLanguage.oPaginate.sFirst;
+				nPrevious.innerHTML = oSettings.oLanguage.oPaginate.sPrevious;
+				nNext.innerHTML = oSettings.oLanguage.oPaginate.sNext;
+				nLast.innerHTML = oSettings.oLanguage.oPaginate.sLast;
+				
+				var oClasses = oSettings.oClasses;
+				nFirst.className = oClasses.sPageButton+" "+oClasses.sPageFirst;
+				nPrevious.className = oClasses.sPageButton+" "+oClasses.sPagePrevious;
+				nNext.className= oClasses.sPageButton+" "+oClasses.sPageNext;
+				nLast.className = oClasses.sPageButton+" "+oClasses.sPageLast;
+				
+				nPaging.appendChild( nFirst );
+				nPaging.appendChild( nPrevious );
+				nPaging.appendChild( nList );
+				nPaging.appendChild( nNext );
+				nPaging.appendChild( nLast );
+				
+				$(nFirst).bind( 'click.DT', function () {
+					if ( oSettings.oApi._fnPageChange( oSettings, "first" ) )
+					{
+						fnCallbackDraw( oSettings );
+					}
+				} );
+				
+				$(nPrevious).bind( 'click.DT', function() {
+					if ( oSettings.oApi._fnPageChange( oSettings, "previous" ) )
+					{
+						fnCallbackDraw( oSettings );
+					}
+				} );
+				
+				$(nNext).bind( 'click.DT', function() {
+					if ( oSettings.oApi._fnPageChange( oSettings, "next" ) )
+					{
+						fnCallbackDraw( oSettings );
+					}
+				} );
+				
+				$(nLast).bind( 'click.DT', function() {
+					if ( oSettings.oApi._fnPageChange( oSettings, "last" ) )
+					{
+						fnCallbackDraw( oSettings );
+					}
+				} );
+				
+				/* Take the brutal approach to cancelling text selection */
+				$('span', nPaging)
+					.bind( 'mousedown.DT', function () { return false; } )
+					.bind( 'selectstart.DT', function () { return false; } );
+				
+				/* ID the first elements only */
+				if ( oSettings.sTableId !== '' && typeof oSettings.aanFeatures.p == "undefined" )
+				{
+					nPaging.setAttribute( 'id', oSettings.sTableId+'_paginate' );
+					nFirst.setAttribute( 'id', oSettings.sTableId+'_first' );
+					nPrevious.setAttribute( 'id', oSettings.sTableId+'_previous' );
+					nNext.setAttribute( 'id', oSettings.sTableId+'_next' );
+					nLast.setAttribute( '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 = _oExt.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;
+				
+				/* Pages calculation */
+				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++ )
+				{
+					if ( iCurrentPage != i )
+					{
+						sList += '<span class="'+oClasses.sPageButton+'">'+i+'</span>';
+					}
+					else
+					{
+						sList += '<span class="'+oClasses.sPageButtonActive+'">'+i+'</span>';
+					}
+				}
+				
+				/* Loop over each instance of the pager */
+				var an = oSettings.aanFeatures.p;
+				var anButtons, anStatic, nPaginateList;
+				var fnClick = function() {
+					/* Use the information in the element to jump to the required page */
+					var iTarget = (this.innerHTML * 1) - 1;
+					oSettings._iDisplayStart = iTarget * oSettings._iDisplayLength;
+					fnCallbackDraw( oSettings );
+					return false;
+				};
+				var fnFalse = function () { return false; };
+				
+				for ( i=0, iLen=an.length ; i<iLen ; i++ )
+				{
+					if ( an[i].childNodes.length === 0 )
+					{
+						continue;
+					}
+					
+					/* Build up the dynamic list forst - html and listeners */
+					var qjPaginateList = $('span:eq(2)', an[i]);
+					qjPaginateList.html( sList );
+					$('span', qjPaginateList).bind( 'click.DT', fnClick ).bind( 'mousedown.DT', fnFalse )
+						.bind( 'selectstart.DT', fnFalse );
+					
+					/* Update the 'premanent botton's classes */
+					anButtons = an[i].getElementsByTagName('span');
+					anStatic = [
+						anButtons[0], anButtons[1], 
+						anButtons[anButtons.length-2], anButtons[anButtons.length-1]
+					];
+					$(anStatic).removeClass( oClasses.sPageButton+" "+oClasses.sPageButtonActive+" "+oClasses.sPageButtonStaticDisabled );
+					if ( iCurrentPage == 1 )
+					{
+						anStatic[0].className += " "+oClasses.sPageButtonStaticDisabled;
+						anStatic[1].className += " "+oClasses.sPageButtonStaticDisabled;
+					}
+					else
+					{
+						anStatic[0].className += " "+oClasses.sPageButton;
+						anStatic[1].className += " "+oClasses.sPageButton;
+					}
+					
+					if ( iPages === 0 || iCurrentPage == iPages || oSettings._iDisplayLength == -1 )
+					{
+						anStatic[2].className += " "+oClasses.sPageButtonStaticDisabled;
+						anStatic[3].className += " "+oClasses.sPageButtonStaticDisabled;
+					}
+					else
+					{
+						anStatic[2].className += " "+oClasses.sPageButton;
+						anStatic[3].className += " "+oClasses.sPageButton;
+					}
+				}
+			}
+		}
+	};
+	
+	/*
+	 * Variable: oSort
+	 * Purpose:  Wrapper for the sorting functions that can be used in DataTables
+	 * Scope:    jQuery.fn.dataTableExt
+	 * Notes:    The functions provided in this object are basically standard javascript sort
+	 *   functions - they expect two inputs which they then compare and then return a priority
+	 *   result. For each sort method added, two functions need to be defined, an ascending sort and
+	 *   a descending sort.
+	 */
+	_oExt.oSort = {
+		/*
+		 * text sorting
+		 */
+		"string-asc": function ( a, b )
+		{
+			var x = a.toLowerCase();
+			var y = b.toLowerCase();
+			return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		},
+		
+		"string-desc": function ( a, b )
+		{
+			var x = a.toLowerCase();
+			var y = b.toLowerCase();
+			return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		},
+		
+		
+		/*
+		 * html sorting (ignore html tags)
+		 */
+		"html-asc": function ( a, b )
+		{
+			var x = a.replace( /<.*?>/g, "" ).toLowerCase();
+			var y = b.replace( /<.*?>/g, "" ).toLowerCase();
+			return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+		},
+		
+		"html-desc": function ( a, b )
+		{
+			var x = a.replace( /<.*?>/g, "" ).toLowerCase();
+			var y = b.replace( /<.*?>/g, "" ).toLowerCase();
+			return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+		},
+		
+		
+		/*
+		 * date sorting
+		 */
+		"date-asc": function ( a, b )
+		{
+			var x = Date.parse( a );
+			var y = Date.parse( b );
+			
+			if ( isNaN(x) || x==="" )
+			{
+    		x = Date.parse( "01/01/1970 00:00:00" );
+			}
+			if ( isNaN(y) || y==="" )
+			{
+				y =	Date.parse( "01/01/1970 00:00:00" );
+			}
+			
+			return x - y;
+		},
+		
+		"date-desc": function ( a, b )
+		{
+			var x = Date.parse( a );
+			var y = Date.parse( b );
+			
+			if ( isNaN(x) || x==="" )
+			{
+    		x = Date.parse( "01/01/1970 00:00:00" );
+			}
+			if ( isNaN(y) || y==="" )
+			{
+				y =	Date.parse( "01/01/1970 00:00:00" );
+			}
+			
+			return y - x;
+		},
+		
+		
+		/*
+		 * numerical sorting
+		 */
+		"numeric-asc": function ( a, b )
+		{
+			var x = (a=="-" || a==="") ? 0 : a*1;
+			var y = (b=="-" || b==="") ? 0 : b*1;
+			return x - y;
+		},
+		
+		"numeric-desc": function ( a, b )
+		{
+			var x = (a=="-" || a==="") ? 0 : a*1;
+			var y = (b=="-" || b==="") ? 0 : b*1;
+			return y - x;
+		}
+	};
+	
+	
+	/*
+	 * Variable: aTypes
+	 * Purpose:  Container for the various type of type detection that dataTables supports
+	 * Scope:    jQuery.fn.dataTableExt
+	 * Notes:    The functions in this array are expected to parse a string to see if it is a data
+	 *   type that it recognises. If so then the function should return the name of the type (a
+	 *   corresponding sort function should be defined!), if the type is not recognised then the
+	 *   function should return null such that the parser and move on to check the next type.
+	 *   Note that ordering is important in this array - the functions are processed linearly,
+	 *   starting at index 0.
+	 *   Note that the input for these functions is always a string! It cannot be any other data
+	 *   type
+	 */
+	_oExt.aTypes = [
+		/*
+		 * Function: -
+		 * Purpose:  Check to see if a string is numeric
+		 * Returns:  string:'numeric' or null
+		 * Inputs:   string:sText - string to check
+		 */
+		function ( sData )
+		{
+			/* Allow zero length strings as a number */
+			if ( sData.length === 0 )
+			{
+				return 'numeric';
+			}
+			
+			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)) || 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 ( sData.indexOf('<') != -1 && sData.indexOf('>') != -1 )
+			{
+				return 'html';
+			}
+			return null;
+		}
+	];
+	
+	/*
+	 * Function: fnVersionCheck
+	 * Purpose:  Check a version string against this version of DataTables. Useful for plug-ins
+	 * Returns:  bool:true -this version of DataTables is greater or equal to the required version
+	 *                false -this version of DataTales is not suitable
+	 * Inputs:   string:sVersion - the version to check against. May be in the following formats:
+	 *             "a", "a.b" or "a.b.c"
+	 * Notes:    This function will only check the first three parts of a version string. It is
+	 *   assumed that beta and dev versions will meet the requirements. This might change in future
+	 */
+	_oExt.fnVersionCheck = function( sVersion )
+	{
+		/* This is cheap, but very effective */
+		var fnZPad = function (Zpad, count)
+		{
+			while(Zpad.length < count) {
+				Zpad += '0';
+			}
+			return Zpad;
+		};
+		var aThis = _oExt.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);
+	};
+	
+	/*
+	 * Variable: _oExternConfig
+	 * Purpose:  Store information for DataTables to access globally about other instances
+	 * Scope:    jQuery.fn.dataTableExt
+	 */
+	_oExt._oExternConfig = {
+		/* int:iNextUnique - next unique number for an instance */
+		"iNextUnique": 0
+	};
+	
+	
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Section - DataTables prototype
+	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+	
+	/*
+	 * Function: dataTable
+	 * Purpose:  DataTables information
+	 * Returns:  -
+	 * Inputs:   object:oInit - initalisation options for the table
+	 */
+	$.fn.dataTable = function( oInit )
+	{
+		/*
+		 * Function: classSettings
+		 * Purpose:  Settings container function for all 'class' properties which are required
+		 *   by dataTables
+		 * Returns:  -
+		 * Inputs:   -
+		 */
+		function classSettings ()
+		{
+			this.fnRecordsTotal = function ()
+			{
+				if ( this.oFeatures.bServerSide ) {
+					return parseInt(this._iRecordsTotal, 10);
+				} else {
+					return this.aiDisplayMaster.length;
+				}
+			};
+			
+			this.fnRecordsDisplay = function ()
+			{
+				if ( this.oFeatures.bServerSide ) {
+					return parseInt(this._iRecordsDisplay, 10);
+				} else {
+					return this.aiDisplay.length;
+				}
+			};
+			
+			this.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;
+				}
+			};
+			
+			/*
+			 * Variable: oInstance
+			 * Purpose:  The DataTables object for this table
+			 * Scope:    jQuery.dataTable.classSettings 
+			 */
+			this.oInstance = null;
+			
+			/*
+			 * Variable: sInstance
+			 * Purpose:  Unique idendifier for each instance of the DataTables object
+			 * Scope:    jQuery.dataTable.classSettings 
+			 */
+			this.sInstance = null;
+			
+			/*
+			 * Variable: oFeatures
+			 * Purpose:  Indicate the enablement of key dataTable features
+			 * Scope:    jQuery.dataTable.classSettings 
+			 */
+			this.oFeatures = {
+				"bPaginate": true,
+				"bLengthChange": true,
+				"bFilter": true,
+				"bSort": true,
+				"bInfo": true,
+				"bAutoWidth": true,
+				"bProcessing": false,
+				"bSortClasses": true,
+				"bStateSave": false,
+				"bServerSide": false
+			};
+			
+			/*
+			 * Variable: oScroll
+			 * Purpose:  Container for scrolling options
+			 * Scope:    jQuery.dataTable.classSettings 
+			 */
+			this.oScroll = {
+				"sX": "",
+				"sXInner": "",
+				"sY": "",
+				"bCollapse": false,
+				"bInfinite": false,
+				"iLoadGap": 100,
+				"iBarWidth": 0,
+				"bAutoCss": true
+			};
+			
+			/*
+			 * Variable: aanFeatures
+			 * Purpose:  Array referencing the nodes which are used for the features
+			 * Scope:    jQuery.dataTable.classSettings 
+			 * Notes:    The parameters of this object match what is allowed by sDom - i.e.
+			 *   'l' - Length changing
+			 *   'f' - Filtering input
+			 *   't' - The table!
+			 *   'i' - Information
+			 *   'p' - Pagination
+			 *   'r' - pRocessing
+			 */
+			this.aanFeatures = [];
+			
+			/*
+			 * Variable: oLanguage
+			 * Purpose:  Store the language strings used by dataTables
+			 * Scope:    jQuery.dataTable.classSettings
+			 * Notes:    The words in the format _VAR_ are variables which are dynamically replaced
+			 *   by javascript
+			 */
+			this.oLanguage = {
+				"sProcessing": "Processing...",
+				"sLengthMenu": "Show _MENU_ entries",
+				"sZeroRecords": "No matching records found",
+				"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": "",
+				"sSearch": "Search:",
+				"sUrl": "",
+				"oPaginate": {
+					"sFirst":    "First",
+					"sPrevious": "Previous",
+					"sNext":     "Next",
+					"sLast":     "Last"
+				},
+				"fnInfoCallback": null
+			};
+			
+			/*
+			 * Variable: aoData
+			 * Purpose:  Store data information
+			 * Scope:    jQuery.dataTable.classSettings 
+			 * Notes:    This is an array of objects with the following parameters:
+			 *   int: _iId - internal id for tracking
+			 *   array: _aData - internal data - used for sorting / filtering etc
+			 *   node: nTr - display node
+			 *   array node: _anHidden - hidden TD nodes
+			 *   string: _sRowStripe
+			 */
+			this.aoData = [];
+			
+			/*
+			 * Variable: aiDisplay
+			 * Purpose:  Array of indexes which are in the current display (after filtering etc)
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.aiDisplay = [];
+			
+			/*
+			 * Variable: aiDisplayMaster
+			 * Purpose:  Array of indexes for display - no filtering
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.aiDisplayMaster = [];
+							
+			/*
+			 * Variable: aoColumns
+			 * Purpose:  Store information about each column that is in use
+			 * Scope:    jQuery.dataTable.classSettings 
+			 */
+			this.aoColumns = [];
+			
+			/*
+			 * Variable: iNextId
+			 * Purpose:  Store the next unique id to be used for a new row
+			 * Scope:    jQuery.dataTable.classSettings 
+			 */
+			this.iNextId = 0;
+			
+			/*
+			 * Variable: asDataSearch
+			 * Purpose:  Search data array for regular expression searching
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.asDataSearch = [];
+			
+			/*
+			 * Variable: oPreviousSearch
+			 * Purpose:  Store the previous search incase we want to force a re-search
+			 *   or compare the old search to a new one
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.oPreviousSearch = {
+				"sSearch": "",
+				"bRegex": false,
+				"bSmart": true
+			};
+			
+			/*
+			 * Variable: aoPreSearchCols
+			 * Purpose:  Store the previous search for each column
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.aoPreSearchCols = [];
+			
+			/*
+			 * Variable: aaSorting
+			 * Purpose:  Sorting information
+			 * Scope:    jQuery.dataTable.classSettings
+			 * Notes:    Index 0 - column number
+			 *           Index 1 - current sorting direction
+			 *           Index 2 - index of asSorting for this column
+			 */
+			this.aaSorting = [ [0, 'asc', 0] ];
+			
+			/*
+			 * Variable: aaSortingFixed
+			 * Purpose:  Sorting information that is always applied
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.aaSortingFixed = null;
+			
+			/*
+			 * Variable: asStripClasses
+			 * Purpose:  Classes to use for the striping of a table
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.asStripClasses = [];
+			
+			/*
+			 * Variable: asDestoryStrips
+			 * Purpose:  If restoring a table - we should restore it's striping classes as well
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.asDestoryStrips = [];
+			
+			/*
+			 * Variable: sDestroyWidth
+			 * Purpose:  If restoring a table - we should restore it's width
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.sDestroyWidth = 0;
+			
+			/*
+			 * Variable: fnRowCallback
+			 * Purpose:  Call this function every time a row is inserted (draw)
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.fnRowCallback = null;
+			
+			/*
+			 * Variable: fnHeaderCallback
+			 * Purpose:  Callback function for the header on each draw
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.fnHeaderCallback = null;
+			
+			/*
+			 * Variable: fnFooterCallback
+			 * Purpose:  Callback function for the footer on each draw
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.fnFooterCallback = null;
+			
+			/*
+			 * Variable: aoDrawCallback
+			 * Purpose:  Array of callback functions for draw callback functions
+			 * Scope:    jQuery.dataTable.classSettings
+			 * Notes:    Each array element is an object with the following parameters:
+			 *   function:fn - function to call
+			 *   string:sName - name callback (feature). useful for arranging array
+			 */
+			this.aoDrawCallback = [];
+			
+			/*
+			 * Variable: fnInitComplete
+			 * Purpose:  Callback function for when the table has been initalised
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.fnInitComplete = null;
+			
+			/*
+			 * Variable: sTableId
+			 * Purpose:  Cache the table ID for quick access
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.sTableId = "";
+			
+			/*
+			 * Variable: nTable
+			 * Purpose:  Cache the table node for quick access
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.nTable = null;
+			
+			/*
+			 * Variable: nTHead
+			 * Purpose:  Permanent ref to the thead element
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.nTHead = null;
+			
+			/*
+			 * Variable: nTFoot
+			 * Purpose:  Permanent ref to the tfoot element - if it exists
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.nTFoot = null;
+			
+			/*
+			 * Variable: nTBody
+			 * Purpose:  Permanent ref to the tbody element
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.nTBody = null;
+			
+			/*
+			 * Variable: nTableWrapper
+			 * Purpose:  Cache the wrapper node (contains all DataTables controlled elements)
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.nTableWrapper = null;
+			
+			/*
+			 * Variable: bInitialised
+			 * Purpose:  Indicate if all required information has been read in
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.bInitialised = false;
+			
+			/*
+			 * Variable: aoOpenRows
+			 * Purpose:  Information about open rows
+			 * Scope:    jQuery.dataTable.classSettings
+			 * Notes:    Has the parameters 'nTr' and 'nParent'
+			 */
+			this.aoOpenRows = [];
+			
+			/*
+			 * Variable: sDom
+			 * Purpose:  Dictate the positioning that the created elements will take
+			 * Scope:    jQuery.dataTable.classSettings
+			 * Notes:    
+			 *   The following options are allowed:
+			 *     'l' - Length changing
+			 *     'f' - Filtering input
+			 *     't' - The table!
+			 *     'i' - Information
+			 *     'p' - Pagination
+			 *     'r' - pRocessing
+			 *   The following constants are allowed:
+			 *     'H' - jQueryUI theme "header" classes
+			 *     'F' - jQueryUI theme "footer" classes
+			 *   The following syntax is expected:
+			 *     '<' and '>' - div elements
+			 *     '<"class" and '>' - div with a class
+			 *   Examples:
+			 *     '<"wrapper"flipt>', '<lf<t>ip>'
+			 */
+			this.sDom = 'lfrtip';
+			
+			/*
+			 * Variable: sPaginationType
+			 * Purpose:  Note which type of sorting should be used
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.sPaginationType = "two_button";
+			
+			/*
+			 * Variable: iCookieDuration
+			 * Purpose:  The cookie duration (for bStateSave) in seconds - default 2 hours
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.iCookieDuration = 60 * 60 * 2;
+			
+			/*
+			 * Variable: sCookiePrefix
+			 * Purpose:  The cookie name prefix
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.sCookiePrefix = "SpryMedia_DataTables_";
+			
+			/*
+			 * Variable: fnCookieCallback
+			 * Purpose:  Callback function for cookie creation
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.fnCookieCallback = null;
+			
+			/*
+			 * Variable: aoStateSave
+			 * Purpose:  Array of callback functions for state saving
+			 * Scope:    jQuery.dataTable.classSettings
+			 * Notes:    Each array element is an object with the following parameters:
+			 *   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]')
+			 *   string:sName - name of callback
+			 */
+			this.aoStateSave = [];
+			
+			/*
+			 * Variable: aoStateLoad
+			 * Purpose:  Array of callback functions for state loading
+			 * Scope:    jQuery.dataTable.classSettings
+			 * Notes:    Each array element is an object with the following parameters:
+			 *   function:fn - function to call. Takes two parameters, oSettings and the object stored.
+			 *     May return false to cancel state loading.
+			 *   string:sName - name of callback
+			 */
+			this.aoStateLoad = [];
+			
+			/*
+			 * Variable: oLoadedState
+			 * Purpose:  State that was loaded from the cookie. Useful for back reference
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.oLoadedState = null;
+			
+			/*
+			 * Variable: sAjaxSource
+			 * Purpose:  Source url for AJAX data for the table
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.sAjaxSource = null;
+			
+			/*
+			 * Variable: bAjaxDataGet
+			 * Purpose:  Note if draw should be blocked while getting data
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.bAjaxDataGet = true;
+			
+			/*
+			 * Variable: fnServerData
+			 * Purpose:  Function to get the server-side data - can be overruled by the developer
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.fnServerData = function ( url, data, callback ) {
+				$.ajax( {
+					"url": url,
+					"data": data,
+					"success": callback,
+					"dataType": "json",
+					"cache": false,
+					"error": function (xhr, error, thrown) {
+						if ( error == "parsererror" ) {
+							alert( "DataTables warning: JSON data from server could not be parsed. "+
+								"This is caused by a JSON formatting error." );
+						}
+					}
+				} );
+			};
+			
+			/*
+			 * Variable: fnFormatNumber
+			 * Purpose:  Format numbers for display
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.fnFormatNumber = function ( iIn )
+			{
+				if ( iIn < 1000 )
+				{
+					/* A small optimisation for what is likely to be the vast majority of use cases */
+					return iIn;
+				}
+				else
+				{
+					var s=(iIn+""), a=s.split(""), out="", iLen=s.length;
+					
+					for ( var i=0 ; i<iLen ; i++ )
+					{
+						if ( i%3 === 0 && i !== 0 )
+						{
+							out = ','+out;
+						}
+						out = a[iLen-i-1]+out;
+					}
+				}
+				return out;
+			};
+			
+			/*
+			 * Variable: aLengthMenu
+			 * Purpose:  List of options that can be used for the user selectable length menu
+			 * Scope:    jQuery.dataTable.classSettings
+			 * Note:     This varaible can take for form of a 1D array, in which case the value and the 
+			 *   displayed value in the menu are the same, or a 2D array in which case the value comes
+			 *   from the first array, and the displayed value to the end user comes from the second
+			 *   array. 2D example: [ [ 10, 25, 50, 100, -1 ], [ 10, 25, 50, 100, 'All' ] ];
+			 */
+			this.aLengthMenu = [ 10, 25, 50, 100 ];
+			
+			/*
+			 * Variable: iDraw
+			 * Purpose:  Counter for the draws that the table does. Also used as a tracker for
+			 *   server-side processing
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.iDraw = 0;
+			
+			/*
+			 * Variable: bDrawing
+			 * Purpose:  Indicate if a redraw is being done - useful for Ajax
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.bDrawing = 0;
+			
+			/*
+			 * Variable: iDrawError
+			 * Purpose:  Last draw error
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.iDrawError = -1;
+			
+			/*
+			 * Variable: _iDisplayLength, _iDisplayStart, _iDisplayEnd
+			 * Purpose:  Display length variables
+			 * Scope:    jQuery.dataTable.classSettings
+			 * Notes:    These variable must NOT be used externally to get the data length. Rather, use
+			 *   the fnRecordsTotal() (etc) functions.
+			 */
+			this._iDisplayLength = 10;
+			this._iDisplayStart = 0;
+			this._iDisplayEnd = 10;
+			
+			/*
+			 * Variable: _iRecordsTotal, _iRecordsDisplay
+			 * Purpose:  Display length variables used for server side processing
+			 * Scope:    jQuery.dataTable.classSettings
+			 * Notes:    These variable must NOT be used externally to get the data length. Rather, use
+			 *   the fnRecordsTotal() (etc) functions.
+			 */
+			this._iRecordsTotal = 0;
+			this._iRecordsDisplay = 0;
+			
+			/*
+			 * Variable: bJUI
+			 * Purpose:  Should we add the markup needed for jQuery UI theming?
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.bJUI = false;
+			
+			/*
+			 * Variable: bJUI
+			 * Purpose:  Should we add the markup needed for jQuery UI theming?
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.oClasses = _oExt.oStdClasses;
+			
+			/*
+			 * Variable: bFiltered and bSorted
+			 * Purpose:  Flags to allow callback functions to see what actions have been performed
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.bFiltered = false;
+			this.bSorted = false;
+			
+			/*
+			 * Variable: oInit
+			 * Purpose:  Initialisation object that is used for the table
+			 * Scope:    jQuery.dataTable.classSettings
+			 */
+			this.oInit = null;
+		}
+		
+		/*
+		 * Variable: oApi
+		 * Purpose:  Container for publicly exposed 'private' functions
+		 * Scope:    jQuery.dataTable
+		 */
+		this.oApi = {};
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - API functions
+		 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+		
+		/*
+		 * Function: fnDraw
+		 * Purpose:  Redraw the table
+		 * Returns:  -
+		 * Inputs:   bool:bComplete - Refilter and resort (if enabled) the table before the draw.
+		 *             Optional: default - true
+		 */
+		this.fnDraw = function( bComplete )
+		{
+			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
+			if ( typeof bComplete != 'undefined' && bComplete === false )
+			{
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+			else
+			{
+				_fnReDraw( oSettings );
+			}
+		};
+		
+		/*
+		 * Function: fnFilter
+		 * Purpose:  Filter the input based on data
+		 * Returns:  -
+		 * Inputs:   string:sInput - string to filter the table on
+		 *           int:iColumn - optional - column to limit filtering to
+		 *           bool:bRegex - optional - treat as regular expression or not - default false
+		 *           bool:bSmart - optional - perform smart filtering or not - default true
+		 *           bool:bShowGlobal - optional - show the input global filter in it's input box(es)
+		 *              - default true
+		 */
+		this.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal )
+		{
+			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
+			
+			if ( !oSettings.oFeatures.bFilter )
+			{
+				return;
+			}
+			
+			if ( typeof bRegex == 'undefined' )
+			{
+				bRegex = false;
+			}
+			
+			if ( typeof bSmart == 'undefined' )
+			{
+				bSmart = true;
+			}
+			
+			if ( typeof bShowGlobal == 'undefined' )
+			{
+				bShowGlobal = true;
+			}
+			
+			if ( typeof iColumn == "undefined" || iColumn === null )
+			{
+				/* Global filter */
+				_fnFilterComplete( oSettings, {
+					"sSearch":sInput,
+					"bRegex": bRegex,
+					"bSmart": bSmart
+				}, 1 );
+				
+				if ( bShowGlobal && typeof oSettings.aanFeatures.f != 'undefined' )
+				{
+					var n = oSettings.aanFeatures.f;
+					for ( var i=0, iLen=n.length ; i<iLen ; i++ )
+					{
+						$('input', n[i]).val( sInput );
+					}
+				}
+			}
+			else
+			{
+				/* Single column filter */
+				oSettings.aoPreSearchCols[ iColumn ].sSearch = sInput;
+				oSettings.aoPreSearchCols[ iColumn ].bRegex = bRegex;
+				oSettings.aoPreSearchCols[ iColumn ].bSmart = bSmart;
+				_fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 );
+			}
+		};
+		
+		/*
+		 * Function: fnSettings
+		 * Purpose:  Get the settings for a particular table for extern. manipulation
+		 * Returns:  -
+		 * Inputs:   -
+		 */
+		this.fnSettings = function( nNode  )
+		{
+			return _fnSettingsFromNode( this[_oExt.iApiIndex] );
+		};
+		
+		/*
+		 * Function: fnVersionCheck
+		 * Notes:    The function is the same as the 'static' function provided in the ext variable
+		 */
+		this.fnVersionCheck = _oExt.fnVersionCheck;
+		
+		/*
+		 * Function: fnSort
+		 * Purpose:  Sort the table by a particular row
+		 * Returns:  -
+		 * Inputs:   int:iCol - the data index to sort on. Note that this will
+		 *   not match the 'display index' if you have hidden data entries
+		 */
+		this.fnSort = function( aaSort )
+		{
+			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
+			oSettings.aaSorting = aaSort;
+			_fnSort( oSettings );
+		};
+		
+		/*
+		 * Function: fnSortListener
+		 * Purpose:  Attach a sort listener to an element for a given column
+		 * Returns:  -
+		 * Inputs:   node:nNode - the element to attach the sort listener to
+		 *           int:iColumn - the column that a click on this node will sort on
+		 *           function:fnCallback - callback function when sort is run - optional
+		 */
+		this.fnSortListener = function( nNode, iColumn, fnCallback )
+		{
+			_fnSortAttachListener( _fnSettingsFromNode( this[_oExt.iApiIndex] ), nNode, iColumn,
+			 	fnCallback );
+		};
+		
+		/*
+		 * Function: fnAddData
+		 * Purpose:  Add new row(s) into the table
+		 * Returns:  array int: array of indexes (aoData) which have been added (zero length on error)
+		 * Inputs:   array:mData - the data to be added. The length must match
+		 *               the original data from the DOM
+		 *             or
+		 *             array array:mData - 2D array of data to be added
+		 *           bool:bRedraw - redraw the table or not - default true
+		 * Notes:    Warning - the refilter here will cause the table to redraw
+		 *             starting at zero
+		 * Notes:    Thanks to Yekimov Denis for contributing the basis for this function!
+		 */
+		this.fnAddData = function( mData, bRedraw )
+		{
+			if ( mData.length === 0 )
+			{
+				return [];
+			}
+			
+			var aiReturn = [];
+			var iTest;
+			
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
+			
+			/* Check if we want to add multiple rows or not */
+			if ( typeof mData[0] == "object" )
+			{
+				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 ( typeof bRedraw == 'undefined' || bRedraw )
+			{
+				_fnReDraw( oSettings );
+			}
+			return aiReturn;
+		};
+		
+		/*
+		 * Function: fnDeleteRow
+		 * Purpose:  Remove a row for the table
+		 * Returns:  array:aReturn - the row that was deleted
+		 * Inputs:   mixed:mTarget - 
+		 *             int: - index of aoData to be deleted, or
+		 *             node(TR): - TR element you want to delete
+		 *           function:fnCallBack - callback function - default null
+		 *           bool:bRedraw - redraw the table or not - default true
+		 */
+		this.fnDeleteRow = function( mTarget, fnCallBack, bRedraw )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
+			var i, iAODataIndex;
+			
+			iAODataIndex = (typeof mTarget == 'object') ? 
+				_fnNodeToDataIndex(oSettings, mTarget) : mTarget;
+			
+			/* Return the data array from this row */
+			var oData = oSettings.aoData.splice( iAODataIndex, 1 );
+			
+			/* 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 dislaying the table */
+			if ( oSettings._iDisplayStart >= oSettings.aiDisplay.length )
+			{
+				oSettings._iDisplayStart -= oSettings._iDisplayLength;
+				if ( oSettings._iDisplayStart < 0 )
+				{
+					oSettings._iDisplayStart = 0;
+				}
+			}
+			
+			if ( typeof bRedraw == 'undefined' || bRedraw )
+			{
+				_fnCalculateEnd( oSettings );
+				_fnDraw( oSettings );
+			}
+			
+			return oData;
+		};
+		
+		/*
+		 * Function: fnClearTable
+		 * Purpose:  Quickly and simply clear a table
+		 * Returns:  -
+		 * Inputs:   bool:bRedraw - redraw the table or not - default true
+		 * Notes:    Thanks to Yekimov Denis for contributing the basis for this function!
+		 */
+		this.fnClearTable = function( bRedraw )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
+			_fnClearTable( oSettings );
+			
+			if ( typeof bRedraw == 'undefined' || bRedraw )
+			{
+				_fnDraw( oSettings );
+			}
+		};
+		
+		/*
+		 * Function: fnOpen
+		 * Purpose:  Open a display row (append a row after the row in question)
+		 * Returns:  node:nNewRow - the row opened
+		 * Inputs:   node:nTr - the table row to 'open'
+		 *           string:sHtml - the HTML to put into the row
+		 *           string:sClass - class to give the new TD cell
+		 */
+		this.fnOpen = function( nTr, sHtml, sClass )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
+			
+			/* 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 );
+			nNewCell.innerHTML = sHtml;
+			
+			/* 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;
+		};
+		
+		/*
+		 * Function: fnClose
+		 * Purpose:  Close a display row
+		 * Returns:  int: 0 (success) or 1 (failed)
+		 * Inputs:   node:nTr - the table row to 'close'
+		 */
+		this.fnClose = function( nTr )
+		{
+			/* Find settings from table node */
+			var oSettings = _fnSettingsFromNode( this[_oExt.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;
+		};
+		
+		/*
+		 * Function: fnGetData
+		 * Purpose:  Return an array with the data which is used to make up the table
+		 * Returns:  array array string: 2d data array ([row][column]) or array string: 1d data array
+		 *           or
+		 *           array string (if iRow specified)
+		 * Inputs:   mixed:mRow - optional - if not present, then the full 2D array for the table 
+		 *             if given then:
+		 *               int: - return 1D array for aoData entry of this index
+		 *               node(TR): - return 1D array for this TR element
+		 * Inputs:   int:iRow - optional - if present then the array returned will be the data for
+		 *             the row with the index 'iRow'
+		 */
+		this.fnGetData = function( mRow )
+		{
+			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
+			
+			if ( typeof mRow != 'undefined' )
+			{
+				var iRow = (typeof mRow == 'object') ? 
+					_fnNodeToDataIndex(oSettings, mRow) : mRow;
+				return ( (aRowData = oSettings.aoData[iRow]) ? aRowData._aData : null);
+			}
+			return _fnGetDataMaster( oSettings );
+		};
+		
+		/*
+		 * Function: fnGetNodes
+		 * Purpose:  Return an array with the TR nodes used for drawing the table
+		 * Returns:  array node: TR elements
+		 *           or
+		 *           node (if iRow specified)
+		 * Inputs:   int:iRow - optional - if present then the array returned will be the node for
+		 *             the row with the index 'iRow'
+		 */
+		this.fnGetNodes = function( iRow )
+		{
+			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
+			
+			if ( typeof iRow != 'undefined' )
+			{
+				return ( (aRowData = oSettings.aoData[iRow]) ? aRowData.nTr : null );
+			}
+			return _fnGetTrNodes( oSettings );
+		};
+		
+		/*
+		 * Function: fnGetPosition
+		 * Purpose:  Get the array indexes of a particular cell from it's DOM element
+		 * Returns:  int: - row index, or array[ int, int, int ]: - row index, column index (visible)
+		 *             and column index including hidden columns
+		 * Inputs:   node:nNode - this can either be a TR or a TD in the table, the return is
+		 *             dependent on this input
+		 */
+		this.fnGetPosition = function( nNode )
+		{
+			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
+			var i;
+			
+			if ( nNode.nodeName.toUpperCase() == "TR" )
+			{
+				return _fnNodeToDataIndex(oSettings, nNode);
+			}
+			else if ( nNode.nodeName.toUpperCase() == "TD" )
+			{
+				var iDataIndex = _fnNodeToDataIndex(oSettings, nNode.parentNode);
+				var iCorrector = 0;
+				for ( var j=0 ; j<oSettings.aoColumns.length ; j++ )
+				{
+					if ( oSettings.aoColumns[j].bVisible )
+					{
+						if ( oSettings.aoData[iDataIndex].nTr.getElementsByTagName('td')[j-iCorrector] == nNode )
+						{
+							return [ iDataIndex, j-iCorrector, j ];
+						}
+					}
+					else
+					{
+						iCorrector++;
+					}
+				}
+			}
+			return null;
+		};
+		
+		/*
+		 * Function: fnUpdate
+		 * Purpose:  Update a table cell or row
+		 * Returns:  int: 0 okay, 1 error
+		 * Inputs:   array string 'or' string:mData - data to update the cell/row with
+		 *           mixed:mRow - 
+		 *             int: - index of aoData to be updated, or
+		 *             node(TR): - TR element you want to update
+		 *           int:iColumn - the column to update - optional (not used of mData is 2D)
+		 *           bool:bRedraw - redraw the table or not - default true
+		 *           bool:bAction - perform predraw actions or not (you will want this as 'true' if
+		 *             you have bRedraw as true) - default true
+		 */
+		this.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction )
+		{
+			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
+			var iVisibleColumn;
+			var sDisplay;
+			var iRow = (typeof mRow == 'object') ? 
+				_fnNodeToDataIndex(oSettings, mRow) : mRow;
+			
+			if ( typeof mData != 'object' )
+			{
+				sDisplay = mData;
+				oSettings.aoData[iRow]._aData[iColumn] = sDisplay;
+				
+				if ( oSettings.aoColumns[iColumn].fnRender !== null )
+				{
+					sDisplay = oSettings.aoColumns[iColumn].fnRender( {
+						"iDataRow": iRow,
+						"iDataColumn": iColumn,
+						"aData": oSettings.aoData[iRow]._aData,
+						"oSettings": oSettings
+					} );
+					
+					if ( oSettings.aoColumns[iColumn].bUseRendered )
+					{
+						oSettings.aoData[iRow]._aData[iColumn] = sDisplay;
+					}
+				}
+				
+				iVisibleColumn = _fnColumnIndexToVisible( oSettings, iColumn );
+				if ( iVisibleColumn !== null )
+				{
+					oSettings.aoData[iRow].nTr.getElementsByTagName('td')[iVisibleColumn].innerHTML = 
+						sDisplay;
+				}
+				else
+				{
+					oSettings.aoData[iRow]._anHidden[iColumn].innerHTML = sDisplay;
+				}
+			}
+			else
+			{
+				if ( mData.length != oSettings.aoColumns.length )
+				{
+					_fnLog( oSettings, 0, 'An array passed to fnUpdate must have the same number of '+
+						'columns as the table in question - in this case '+oSettings.aoColumns.length );
+					return 1;
+				}
+				
+				for ( var i=0 ; i<mData.length ; i++ )
+				{
+					sDisplay = mData[i];
+					oSettings.aoData[iRow]._aData[i] = sDisplay;
+					
+					if ( oSettings.aoColumns[i].fnRender !== null )
+					{
+						sDisplay = oSettings.aoColumns[i].fnRender( {
+							"iDataRow": iRow,
+							"iDataColumn": i,
+							"aData": oSettings.aoData[iRow]._aData,
+							"oSettings": oSettings
+						} );
+						
+						if ( oSettings.aoColumns[i].bUseRendered )
+						{
+							oSettings.aoData[iRow]._aData[i] = sDisplay;
+						}
+					}
+					
+					iVisibleColumn = _fnColumnIndexToVisible( oSettings, i );
+					if ( iVisibleColumn !== null )
+					{
+						oSettings.aoData[iRow].nTr.getElementsByTagName('td')[iVisibleColumn].innerHTML = 
+							sDisplay;
+					}
+					else
+					{
+						oSettings.aoData[iRow]._anHidden[i].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, 
+				oSettings.aoData[iRow]._aData );
+			
+			/* Perform pre-draw actions */
+			if ( typeof bAction == 'undefined' || bAction )
+			{
+				_fnAjustColumnSizing( oSettings );
+			}
+			
+			/* Redraw the table */
+			if ( typeof bRedraw == 'undefined' || bRedraw )
+			{
+				_fnReDraw( oSettings );
+			}
+			return 0;
+		};
+		
+		
+		/*
+		 * Function: fnShowColoumn
+		 * Purpose:  Show a particular column
+		 * Returns:  -
+		 * Inputs:   int:iCol - the column whose display should be changed
+		 *           bool:bShow - show (true) or hide (false) the column
+		 *           bool:bRedraw - redraw the table or not - default true
+		 */
+		this.fnSetColumnVis = function ( iCol, bShow, bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
+			var i, iLen;
+			var iColumns = oSettings.aoColumns.length;
+			var nTd, anTds, nCell, anTrs, jqChildren;
+			
+			/* No point in doing anything if we are requesting what is already true */
+			if ( oSettings.aoColumns[iCol].bVisible == bShow )
+			{
+				return;
+			}
+			
+			var nTrHead = $('>tr', oSettings.nTHead)[0];
+			var nTrFoot = $('>tr', oSettings.nTFoot)[0];
+			var anTheadTh = [];
+			var anTfootTh = [];
+			for ( i=0 ; i<iColumns ; i++ )
+			{
+				anTheadTh.push( oSettings.aoColumns[i].nTh );
+				anTfootTh.push( oSettings.aoColumns[i].nTf );
+			}
+			
+			/* Show the column */
+			if ( bShow )
+			{
+				var iInsert = 0;
+				for ( i=0 ; i<iCol ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bVisible )
+					{
+						iInsert++;
+					}
+				}
+				
+				/* Need to decide if we should use appendChild or insertBefore */
+				if ( iInsert >= _fnVisbleColumns( oSettings ) )
+				{
+					nTrHead.appendChild( anTheadTh[iCol] );
+					anTrs = $('>tr', oSettings.nTHead);
+					for ( i=1, iLen=anTrs.length ; i<iLen ; i++ )
+					{
+						anTrs[i].appendChild( oSettings.aoColumns[iCol].anThExtra[i-1] );
+					}	
+					
+					if ( nTrFoot )
+					{
+						nTrFoot.appendChild( anTfootTh[iCol] );
+						anTrs = $('>tr', oSettings.nTFoot);
+						for ( i=1, iLen=anTrs.length ; i<iLen ; i++ )
+						{
+							anTrs[i].appendChild( oSettings.aoColumns[iCol].anTfExtra[i-1] );
+						}	
+					}
+					
+					for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+					{
+						nTd = oSettings.aoData[i]._anHidden[iCol];
+						oSettings.aoData[i].nTr.appendChild( nTd );
+					}
+				}
+				else
+				{
+					/* Which coloumn should we be inserting before? */
+					var iBefore;
+					for ( i=iCol ; i<iColumns ; i++ )
+					{
+						iBefore = _fnColumnIndexToVisible( oSettings, i );
+						if ( iBefore !== null )
+						{
+							break;
+						}
+					}
+					
+					nTrHead.insertBefore( anTheadTh[iCol], nTrHead.getElementsByTagName('th')[iBefore] );
+					anTrs = $('>tr', oSettings.nTHead);
+					for ( i=1, iLen=anTrs.length ; i<iLen ; i++ )
+					{
+						jqChildren = $(anTrs[i]).children();
+						anTrs[i].insertBefore( oSettings.aoColumns[iCol].anThExtra[i-1], jqChildren[iBefore] );
+					}	
+					
+					if ( nTrFoot )
+					{
+						nTrFoot.insertBefore( anTfootTh[iCol], nTrFoot.getElementsByTagName('th')[iBefore] );
+						anTrs = $('>tr', oSettings.nTFoot);
+						for ( i=1, iLen=anTrs.length ; i<iLen ; i++ )
+						{
+							jqChildren = $(anTrs[i]).children();
+							anTrs[i].insertBefore( oSettings.aoColumns[iCol].anTfExtra[i-1], jqChildren[iBefore] );
+						}	
+					}
+					
+					anTds = _fnGetTdNodes( oSettings );
+					for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+					{
+						nTd = oSettings.aoData[i]._anHidden[iCol];
+						oSettings.aoData[i].nTr.insertBefore( nTd, $('>td:eq('+iBefore+')', 
+							oSettings.aoData[i].nTr)[0] );
+					}
+				}
+				
+				oSettings.aoColumns[iCol].bVisible = true;
+			}
+			else
+			{
+				/* Remove a column from display */
+				nTrHead.removeChild( anTheadTh[iCol] );
+				for ( i=0, iLen=oSettings.aoColumns[iCol].anThExtra.length ; i<iLen ; i++ )
+				{
+					nCell = oSettings.aoColumns[iCol].anThExtra[i];
+					nCell.parentNode.removeChild( nCell );
+				}
+				
+				if ( nTrFoot )
+				{
+					nTrFoot.removeChild( anTfootTh[iCol] );
+					for ( i=0, iLen=oSettings.aoColumns[iCol].anTfExtra.length ; i<iLen ; i++ )
+					{
+						nCell = oSettings.aoColumns[iCol].anTfExtra[i];
+						nCell.parentNode.removeChild( nCell );
+					}
+				}
+				
+				anTds = _fnGetTdNodes( oSettings );
+				for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+				{
+					nTd = anTds[ ( i*oSettings.aoColumns.length) + (iCol*1) ];
+					oSettings.aoData[i]._anHidden[iCol] = nTd;
+					nTd.parentNode.removeChild( nTd );
+				}
+				
+				oSettings.aoColumns[iCol].bVisible = false;
+			}
+			
+			/* 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 ( typeof bRedraw == 'undefined' || bRedraw )
+			{
+				_fnAjustColumnSizing( oSettings );
+				_fnDraw( oSettings );
+			}
+			
+			_fnSaveState( oSettings );
+		};
+		
+		/*
+		 * Function: fnPageChange
+		 * Purpose:  Change the pagination
+		 * Returns:  -
+		 * Inputs:   string:sAction - paging action to take: "first", "previous", "next" or "last"
+		 *           bool:bRedraw - redraw the table or not - optional - default true
+		 */
+		this.fnPageChange = function ( sAction, bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
+			_fnPageChange( oSettings, sAction );
+			_fnCalculateEnd( oSettings );
+			
+			if ( typeof bRedraw == 'undefined' || bRedraw )
+			{
+				_fnDraw( oSettings );
+			}
+		};
+		
+		/*
+		 * Function: fnDestroy
+		 * Purpose:  Destructor for the DataTable
+		 * Returns:  -
+		 * Inputs:   -
+		 */
+		this.fnDestroy = function ( )
+		{
+			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );
+			var nOrig = oSettings.nTableWrapper.parentNode;
+			var nBody = oSettings.nTBody;
+			var i, iLen;
+			
+			/* Flag to note that the table is currently being destoryed - no action should be taken */
+			oSettings.bDestroying = true;
+			
+			/* Blitz all DT events */
+			$(oSettings.nTableWrapper).find('*').andSelf().unbind('.DT');
+			
+			/* Restore hidden columns */
+			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bVisible === false )
+				{
+					this.fnSetColumnVis( i, true );
+				}
+			}
+			
+			/* 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 )
+			{
+				$('>thead', oSettings.nTable).remove();
+				oSettings.nTable.appendChild( oSettings.nTHead );
+			}
+			
+			if ( oSettings.nTFoot && oSettings.nTable != oSettings.nTFoot.parentNode )
+			{
+				$('>tfoot', oSettings.nTable).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.asStripClasses.join(' ') );
+			
+			if ( !oSettings.bJUI )
+			{
+				$('th', oSettings.nTHead).removeClass( [ _oExt.oStdClasses.sSortable,
+					_oExt.oStdClasses.sSortableAsc,
+					_oExt.oStdClasses.sSortableDesc,
+					_oExt.oStdClasses.sSortableNone ].join(' ')
+				);
+			}
+			else
+			{
+				$('th', oSettings.nTHead).removeClass( [ _oExt.oStdClasses.sSortable,
+					_oExt.oJUIClasses.sSortableAsc,
+					_oExt.oJUIClasses.sSortableDesc,
+					_oExt.oJUIClasses.sSortableNone ].join(' ')
+				);
+				$('th span', oSettings.nTHead).remove();
+			}
+			
+			/* Add the TR elements back into the table in their original order */
+			nOrig.appendChild( oSettings.nTable );
+			for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+			{
+				nBody.appendChild( oSettings.aoData[i].nTr );
+			}
+			
+			/* Restore the width of the original table */
+			oSettings.nTable.style.width = _fnStringToCss(oSettings.sDestroyWidth);
+			
+			/* If the were originally odd/even type classes - then we add them back here. Note
+			 * this is not fool proof (for example if not all rows as odd/even classes - but 
+			 * it's a good effort without getting carried away
+			 */
+			$('>tr:even', nBody).addClass( oSettings.asDestoryStrips[0] );
+			$('>tr:odd', nBody).addClass( oSettings.asDestoryStrips[1] );
+			
+			/* Remove the settings object from the settings array */
+			for ( i=0, iLen=_aoSettings.length ; i<iLen ; i++ )
+			{
+				if ( _aoSettings[i] == oSettings )
+				{
+					_aoSettings.splice( i, 1 );
+				}
+			}
+			
+			/* End it all */
+			oSettings = null;
+		};
+		
+		/*
+		 * Function: fnAjustColumnSizing
+		 * Purpose:  Update tale sizing based on content. This would most likely be used for scrolling
+		 *   and will typically need a redraw after it.
+		 * Returns:  -
+		 * Inputs:   bool:bRedraw - redraw the table or not, you will typically want to - default true
+		 */
+		this.fnAdjustColumnSizing = function ( bRedraw )
+		{
+			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);
+			_fnAjustColumnSizing( oSettings );
+			
+			if ( typeof 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);
+			}
+		};
+		
+		/*
+		 * Plugin API functions
+		 * 
+		 * This call will add the functions which are defined in _oExt.oApi to the
+		 * DataTables object, providing a rather nice way to allow plug-in API functions. Note that
+		 * this is done here, so that API function can actually override the built in API functions if
+		 * required for a particular purpose.
+		 */
+		
+		/*
+		 * Function: _fnExternApiFunc
+		 * Purpose:  Create a wrapper function for exporting an internal func to an external API func
+		 * Returns:  function: - wrapped function
+		 * Inputs:   string:sFunc - API function name
+		 */
+		function _fnExternApiFunc (sFunc)
+		{
+			return function() {
+					var aArgs = [_fnSettingsFromNode(this[_oExt.iApiIndex])].concat( 
+						Array.prototype.slice.call(arguments) );
+					return _oExt.oApi[sFunc].apply( this, aArgs );
+				};
+		}
+		
+		for ( var sFunc in _oExt.oApi )
+		{
+			if ( sFunc )
+			{
+				/*
+				 * Function: anon
+				 * Purpose:  Wrap the plug-in API functions in order to provide the settings as 1st arg 
+				 *   and execute in this scope
+				 * Returns:  -
+				 * Inputs:   -
+				 */
+				this[sFunc] = _fnExternApiFunc(sFunc);
+			}
+		}
+		
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - Local functions
+		 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - Initalisation
+		 */
+		
+		/*
+		 * Function: _fnInitalise
+		 * Purpose:  Draw the table for the first time, adding all required features
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnInitalise ( oSettings )
+		{
+			var i, iLen;
+			
+			/* Ensure that the table data is fully initialised */
+			if ( oSettings.bInitialised === false )
+			{
+				setTimeout( function(){ _fnInitalise( oSettings ); }, 200 );
+				return;
+			}
+			
+			/* Show the display HTML options */
+			_fnAddOptionsHtml( oSettings );
+			
+			/* Draw the headers for the table */
+			_fnDrawHead( oSettings );
+			
+			/* 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
+			{
+				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 )
+			{
+				oSettings.fnServerData.call( oSettings.oInstance, oSettings.sAjaxSource, [], function(json) {
+					/* Got the data - add it to the table */
+					for ( i=0 ; i<json.aaData.length ; i++ )
+					{
+						_fnAddData( oSettings, json.aaData[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 = oSettings._iDisplayStart;
+					
+					if ( oSettings.oFeatures.bSort )
+					{
+						_fnSort( oSettings );
+					}
+					else
+					{
+						oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+						_fnCalculateEnd( oSettings );
+						_fnDraw( oSettings );
+					}
+					
+					_fnProcessingDisplay( oSettings, false );
+					_fnInitComplete( oSettings, json );
+				} );
+				return;
+			}
+			
+			/* Server-side processing initialisation complete is done at the end of _fnDraw */
+			if ( !oSettings.oFeatures.bServerSide )
+			{
+				_fnProcessingDisplay( oSettings, false );
+				_fnInitComplete( oSettings );
+			}
+		}
+		
+		/*
+		 * Function: _fnInitalise
+		 * Purpose:  Draw the table for the first time, adding all required features
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnInitComplete ( oSettings, json )
+		{
+			oSettings._bInitComplete = true;
+			if ( typeof oSettings.fnInitComplete == 'function' )
+			{
+				if ( typeof json != 'undefined' )
+				{
+					oSettings.fnInitComplete.call( oSettings.oInstance, oSettings, json );
+				}
+				else
+				{
+					oSettings.fnInitComplete.call( oSettings.oInstance, oSettings );
+				}
+			}
+		}
+		
+		/*
+		 * Function: _fnLanguageProcess
+		 * Purpose:  Copy language variables from remote object to a local one
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           object:oLanguage - Language information
+		 *           bool:bInit - init once complete
+		 */
+		function _fnLanguageProcess( oSettings, oLanguage, bInit )
+		{
+			_fnMap( oSettings.oLanguage, oLanguage, 'sProcessing' );
+			_fnMap( oSettings.oLanguage, oLanguage, 'sLengthMenu' );
+			_fnMap( oSettings.oLanguage, oLanguage, 'sEmptyTable' );
+			_fnMap( oSettings.oLanguage, oLanguage, 'sZeroRecords' );
+			_fnMap( oSettings.oLanguage, oLanguage, 'sInfo' );
+			_fnMap( oSettings.oLanguage, oLanguage, 'sInfoEmpty' );
+			_fnMap( oSettings.oLanguage, oLanguage, 'sInfoFiltered' );
+			_fnMap( oSettings.oLanguage, oLanguage, 'sInfoPostFix' );
+			_fnMap( oSettings.oLanguage, oLanguage, 'sSearch' );
+			
+			if ( typeof oLanguage.oPaginate != 'undefined' )
+			{
+				_fnMap( oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sFirst' );
+				_fnMap( oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sPrevious' );
+				_fnMap( oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sNext' );
+				_fnMap( oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sLast' );
+			}
+			
+			/* Backwards compatibility - if there is no sEmptyTable given, then use the same as
+			 * sZeroRecords - assuming that is given.
+			 */
+			if ( typeof oLanguage.sEmptyTable == 'undefined' && 
+			     typeof oLanguage.sZeroRecords != 'undefined' )
+			{
+				_fnMap( oSettings.oLanguage, oLanguage, 'sZeroRecords', 'sEmptyTable' );
+			}
+			
+			if ( bInit )
+			{
+				_fnInitalise( oSettings );
+			}
+		}
+		
+		/*
+		 * Function: _fnAddColumn
+		 * Purpose:  Add a column to the list used for the table with default values
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           node:nTh - the th element for this column
+		 */
+		function _fnAddColumn( oSettings, nTh )
+		{
+			oSettings.aoColumns[ oSettings.aoColumns.length++ ] = {
+				"sType": null,
+				"_bAutoType": true,
+				"bVisible": true,
+				"bSearchable": true,
+				"bSortable": true,
+				"asSorting": [ 'asc', 'desc' ],
+				"sSortingClass": oSettings.oClasses.sSortable,
+				"sSortingClassJUI": oSettings.oClasses.sSortJUI,
+				"sTitle": nTh ? nTh.innerHTML : '',
+				"sName": '',
+				"sWidth": null,
+				"sWidthOrig": null,
+				"sClass": null,
+				"fnRender": null,
+				"bUseRendered": true,
+				"iDataSort": oSettings.aoColumns.length-1,
+				"sSortDataType": 'std',
+				"nTh": nTh ? nTh : document.createElement('th'),
+				"nTf": null,
+				"anThExtra": [],
+				"anTfExtra": []
+			};
+			
+			var iCol = oSettings.aoColumns.length-1;
+			var oCol = oSettings.aoColumns[ iCol ];
+			
+			/* Add a column specific filter */
+			if ( typeof oSettings.aoPreSearchCols[ iCol ] == 'undefined' ||
+			     oSettings.aoPreSearchCols[ iCol ] === null )
+			{
+				oSettings.aoPreSearchCols[ iCol ] = {
+					"sSearch": "",
+					"bRegex": false,
+					"bSmart": true
+				};
+			}
+			else
+			{
+				/* Don't require that the user must specify bRegex and / or bSmart */
+				if ( typeof oSettings.aoPreSearchCols[ iCol ].bRegex == 'undefined' )
+				{
+					oSettings.aoPreSearchCols[ iCol ].bRegex = true;
+				}
+				
+				if ( typeof oSettings.aoPreSearchCols[ iCol ].bSmart == 'undefined' )
+				{
+					oSettings.aoPreSearchCols[ iCol ].bSmart = true;
+				}
+			} 
+			
+			/* Use the column options function to initialise classes etc */
+			_fnColumnOptions( oSettings, iCol, null );
+		}
+		
+		/*
+		 * Function: _fnColumnOptions
+		 * Purpose:  Apply options for a column
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           int:iCol - column index to consider
+		 *           object:oOptions - object with sType, bVisible and bSearchable
+		 */
+		function _fnColumnOptions( oSettings, iCol, oOptions )
+		{
+			var oCol = oSettings.aoColumns[ iCol ];
+			
+			/* User specified column options */
+			if ( typeof oOptions != 'undefined' && oOptions !== null )
+			{
+				if ( typeof oOptions.sType != 'undefined' )
+				{
+					oCol.sType = oOptions.sType;
+					oCol._bAutoType = false;
+				}
+				
+				_fnMap( oCol, oOptions, "bVisible" );
+				_fnMap( oCol, oOptions, "bSearchable" );
+				_fnMap( oCol, oOptions, "bSortable" );
+				_fnMap( oCol, oOptions, "sTitle" );
+				_fnMap( oCol, oOptions, "sName" );
+				_fnMap( oCol, oOptions, "sWidth" );
+				_fnMap( oCol, oOptions, "sWidth", "sWidthOrig" );
+				_fnMap( oCol, oOptions, "sClass" );
+				_fnMap( oCol, oOptions, "fnRender" );
+				_fnMap( oCol, oOptions, "bUseRendered" );
+				_fnMap( oCol, oOptions, "iDataSort" );
+				_fnMap( oCol, oOptions, "asSorting" );
+				_fnMap( oCol, oOptions, "sSortDataType" );
+			}
+			
+			/* 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.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;
+			}
+		}
+		
+		/*
+		 * Function: _fnAddData
+		 * Purpose:  Add a data array to the table, creating DOM node etc
+		 * Returns:  int: - >=0 if successful (index of new aoData entry), -1 if failed
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           array:aData - data array to be added
+		 * Notes:    There are two basic methods for DataTables to get data to display - a JS array
+		 *   (which is dealt with by this function), and the DOM, which has it's own optimised
+		 *   function (_fnGatherData). Be careful to make the same changes here as there and vice-versa
+		 */
+		function _fnAddData ( oSettings, aDataSupplied )
+		{
+			/* Sanity check the length of the new array */
+			if ( aDataSupplied.length != oSettings.aoColumns.length &&
+				oSettings.iDrawError != oSettings.iDraw )
+			{
+				_fnLog( oSettings, 0, "Added data (size "+aDataSupplied.length+") does not match known "+
+					"number of columns ("+oSettings.aoColumns.length+")" );
+				oSettings.iDrawError = oSettings.iDraw;
+				return -1;
+			}
+			
+			
+			/* Create the object for storing information about this new row */
+			var aData = aDataSupplied.slice();
+			var iThisIndex = oSettings.aoData.length;
+			oSettings.aoData.push( {
+				"nTr": document.createElement('tr'),
+				"_iId": oSettings.iNextId++,
+				"_aData": aData,
+				"_anHidden": [],
+				"_sRowStripe": ''
+			} );
+			
+			/* Create the cells */
+			var nTd, sThisType;
+			for ( var i=0 ; i<aData.length ; i++ )
+			{
+				nTd = document.createElement('td');
+				
+				/* Allow null data (from a data array) - simply deal with it as a blank string */
+				if ( aData[i] === null )
+				{
+					aData[i] = '';
+				}
+				
+				if ( typeof oSettings.aoColumns[i].fnRender == 'function' )
+				{
+					var sRendered = oSettings.aoColumns[i].fnRender( {
+							"iDataRow": iThisIndex,
+							"iDataColumn": i,
+							"aData": aData,
+							"oSettings": oSettings
+						} );
+					nTd.innerHTML = sRendered;
+					if ( oSettings.aoColumns[i].bUseRendered )
+					{
+						/* Use the rendered data for filtering/sorting */
+						oSettings.aoData[iThisIndex]._aData[i] = sRendered;
+					}
+				}
+				else
+				{
+					nTd.innerHTML = aData[i];
+				}
+				
+				/* Cast everything as a string - so we can treat everything equally when sorting */
+				if ( typeof aData[i] != 'string' )
+				{
+					aData[i] += "";
+				}
+				aData[i] = $.trim(aData[i]);
+				
+				/* Add user defined class */
+				if ( oSettings.aoColumns[i].sClass !== null )
+				{
+					nTd.className = oSettings.aoColumns[i].sClass;
+				}
+				
+				/* See if we should auto-detect the column type */
+				if ( oSettings.aoColumns[i]._bAutoType && oSettings.aoColumns[i].sType != 'string' )
+				{
+					/* Attempt to auto detect the type - same as _fnGatherData() */
+					sThisType = _fnDetectType( oSettings.aoData[iThisIndex]._aData[i] );
+					if ( oSettings.aoColumns[i].sType === null )
+					{
+						oSettings.aoColumns[i].sType = sThisType;
+					}
+					else if ( oSettings.aoColumns[i].sType != sThisType )
+					{
+						/* String is always the 'fallback' option */
+						oSettings.aoColumns[i].sType = 'string';
+					}
+				}
+					
+				if ( oSettings.aoColumns[i].bVisible )
+				{
+					oSettings.aoData[iThisIndex].nTr.appendChild( nTd );
+					oSettings.aoData[iThisIndex]._anHidden[i] = null;
+				}
+				else
+				{
+					oSettings.aoData[iThisIndex]._anHidden[i] = nTd;
+				}
+			}
+			
+			/* Add to the display array */
+			oSettings.aiDisplayMaster.push( iThisIndex );
+			return iThisIndex;
+		}
+		
+		/*
+		 * Function: _fnGatherData
+		 * Purpose:  Read in the data from the target table from the DOM
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 * Notes:    This is a optimised version of _fnAddData (more or less) for reading information
+		 *   from the DOM. The basic actions must be identical in the two functions.
+		 */
+		function _fnGatherData( oSettings )
+		{
+			var iLoop, i, iLen, j, jLen, jInner,
+			 	nTds, nTrs, nTd, aLocalData, iThisIndex,
+				iRow, iRows, iColumn, iColumns;
+			
+			/*
+			 * 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.sAjaxSource === null )
+			{
+				nTrs = oSettings.nTBody.childNodes;
+				for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+				{
+					if ( nTrs[i].nodeName.toUpperCase() == "TR" )
+					{
+						iThisIndex = oSettings.aoData.length;
+						oSettings.aoData.push( {
+							"nTr": nTrs[i],
+							"_iId": oSettings.iNextId++,
+							"_aData": [],
+							"_anHidden": [],
+							"_sRowStripe": ''
+						} );
+						
+						oSettings.aiDisplayMaster.push( iThisIndex );
+						
+						aLocalData = oSettings.aoData[iThisIndex]._aData;
+						nTds = nTrs[i].childNodes;
+						jInner = 0;
+						
+						for ( j=0, jLen=nTds.length ; j<jLen ; j++ )
+						{
+							if ( nTds[j].nodeName.toUpperCase() == "TD" )
+							{
+								aLocalData[jInner] = $.trim(nTds[j].innerHTML);
+								jInner++;
+							}
+						}
+					}
+				}
+			}
+			
+			/* 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++ )
+			{
+				for ( j=0, jLen=nTrs[i].childNodes.length ; j<jLen ; j++ )
+				{
+					nTd = nTrs[i].childNodes[j];
+					if ( nTd.nodeName.toUpperCase() == "TD" )
+					{
+						nTds.push( nTd );
+					}
+				}
+			}
+			
+			/* Sanity check */
+			if ( nTds.length != nTrs.length * oSettings.aoColumns.length )
+			{
+				_fnLog( oSettings, 1, "Unexpected number of TD elements. Expected "+
+					(nTrs.length * oSettings.aoColumns.length)+" and got "+nTds.length+". DataTables does "+
+					"not support rowspan / colspan in the table body, and there must be one cell for each "+
+					"row/column combination." );
+			}
+			
+			/* Now process by column */
+			for ( iColumn=0, iColumns=oSettings.aoColumns.length ; iColumn<iColumns ; iColumn++ )
+			{
+				/* Get the title of the column - unless there is a user set one */
+				if ( oSettings.aoColumns[iColumn].sTitle === null )
+				{
+					oSettings.aoColumns[iColumn].sTitle = oSettings.aoColumns[iColumn].nTh.innerHTML;
+				}
+				
+				var
+					bAutoType = oSettings.aoColumns[iColumn]._bAutoType,
+					bRender = typeof oSettings.aoColumns[iColumn].fnRender == 'function',
+					bClass = oSettings.aoColumns[iColumn].sClass !== null,
+					bVisible = oSettings.aoColumns[iColumn].bVisible,
+					nCell, sThisType, sRendered;
+				
+				/* 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++ )
+					{
+						nCell = nTds[ (iRow*iColumns) + iColumn ];
+						
+						/* Type detection */
+						if ( bAutoType )
+						{
+							if ( oSettings.aoColumns[iColumn].sType != 'string' )
+							{
+								sThisType = _fnDetectType( oSettings.aoData[iRow]._aData[iColumn] );
+								if ( oSettings.aoColumns[iColumn].sType === null )
+								{
+									oSettings.aoColumns[iColumn].sType = sThisType;
+								}
+								else if ( oSettings.aoColumns[iColumn].sType != sThisType )
+								{
+									/* String is always the 'fallback' option */
+									oSettings.aoColumns[iColumn].sType = 'string';
+								}
+							}
+						}
+						
+						/* Rendering */
+						if ( bRender )
+						{
+							sRendered = oSettings.aoColumns[iColumn].fnRender( {
+									"iDataRow": iRow,
+									"iDataColumn": iColumn,
+									"aData": oSettings.aoData[iRow]._aData,
+									"oSettings": oSettings
+								} );
+							nCell.innerHTML = sRendered;
+							if ( oSettings.aoColumns[iColumn].bUseRendered )
+							{
+								/* Use the rendered data for filtering/sorting */
+								oSettings.aoData[iRow]._aData[iColumn] = sRendered;
+							}
+						}
+						
+						/* Classes */
+						if ( bClass )
+						{
+							nCell.className += ' '+oSettings.aoColumns[iColumn].sClass;
+						}
+						
+						/* Column visability */
+						if ( !bVisible )
+						{
+							oSettings.aoData[iRow]._anHidden[iColumn] = nCell;
+							nCell.parentNode.removeChild( nCell );
+						}
+						else
+						{
+							oSettings.aoData[iRow]._anHidden[iColumn] = null;
+						}
+					}
+				}
+			}
+		}
+		
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - Drawing functions
+		 */
+		
+		/*
+		 * Function: _fnDrawHead
+		 * Purpose:  Create the HTML header for the table
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnDrawHead( oSettings )
+		{
+			var i, nTh, iLen, j, jLen;
+			var anTr = oSettings.nTHead.getElementsByTagName('tr');
+			var iThs = oSettings.nTHead.getElementsByTagName('th').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;
+					
+					if ( oSettings.aoColumns[i].sClass !== null )
+					{
+						$(nTh).addClass( oSettings.aoColumns[i].sClass );
+					}
+					
+					/* Cache and remove (if needed) any extra elements for this column in the header */
+					for ( j=1, jLen=anTr.length ; j<jLen ; j++ )
+					{
+						jqChildren = $(anTr[j]).children();
+						oSettings.aoColumns[i].anThExtra.push( jqChildren[i-iCorrector] );
+						if ( !oSettings.aoColumns[i].bVisible )
+						{
+							anTr[j].removeChild( jqChildren[i-iCorrector] );
+						}
+					}
+					
+					if ( oSettings.aoColumns[i].bVisible )
+					{
+						/* 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
+					{
+						nTh.parentNode.removeChild( nTh );
+						iCorrector++;
+					}
+				}
+			}
+			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;
+					
+					if ( oSettings.aoColumns[i].sClass !== null )
+					{
+						$(nTh).addClass( oSettings.aoColumns[i].sClass );
+					}
+					
+					if ( oSettings.aoColumns[i].bVisible )
+					{
+						nTr.appendChild( nTh );
+					}
+				}
+				$(oSettings.nTHead).html( '' )[0].appendChild( nTr );
+			}
+			
+			/* 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);
+					
+					nDiv.appendChild( document.createElement('span') );
+					nTh.appendChild( nDiv );
+				}
+			}
+			
+			/* Add sort listener */
+			var fnNoSelect = function (e) {
+				this.onselectstart = function() { return false; };
+				return false;
+			};
+			
+			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 );
+						
+						/* Take the brutal approach to cancelling text selection in header */
+						$(oSettings.aoColumns[i].nTh).bind( 'mousedown.DT', fnNoSelect );
+					}
+					else
+					{
+						$(oSettings.aoColumns[i].nTh).addClass( oSettings.oClasses.sSortableNone );
+					}
+				}
+			}
+			
+			/* Cache the footer elements */
+			if ( oSettings.nTFoot !== null )
+			{
+				iCorrector = 0;
+				anTr = oSettings.nTFoot.getElementsByTagName('tr');
+				var nTfs = anTr[0].getElementsByTagName('th');
+				
+				for ( i=0, iLen=nTfs.length ; i<iLen ; i++ )
+				{
+					if ( typeof oSettings.aoColumns[i] != 'undefined' )
+					{
+						oSettings.aoColumns[i].nTf = nTfs[i-iCorrector];
+						
+						if ( oSettings.oClasses.sFooterTH !== "" )
+						{
+							oSettings.aoColumns[i].nTf.className += " "+oSettings.oClasses.sFooterTH;
+						}
+						
+						/* Deal with any extra elements for this column from the footer */
+						for ( j=1, jLen=anTr.length ; j<jLen ; j++ )
+						{
+							jqChildren = $(anTr[j]).children();
+							oSettings.aoColumns[i].anTfExtra.push( jqChildren[i-iCorrector] );
+							if ( !oSettings.aoColumns[i].bVisible )
+							{
+								anTr[j].removeChild( jqChildren[i-iCorrector] );
+							}
+						}
+						
+						if ( !oSettings.aoColumns[i].bVisible )
+						{
+							nTfs[i-iCorrector].parentNode.removeChild( nTfs[i-iCorrector] );
+							iCorrector++;
+						}
+					}
+				}
+			}
+		}
+		
+		/*
+		 * Function: _fnDraw
+		 * Purpose:  Insert the required TR nodes into the table for display
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnDraw( oSettings )
+		{
+			var i, iLen;
+			var anRows = [];
+			var iRowCount = 0;
+			var bRowError = false;
+			var iStrips = oSettings.asStripClasses.length;
+			var iOpenRows = oSettings.aoOpenRows.length;
+			
+			oSettings.bDrawing = true;
+			
+			/* Check and see if we have an initial draw position from state saving */
+			if ( typeof 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 );
+			}
+			
+			/* If we are dealing with Ajax - do it here */
+			if ( !oSettings.bDestroying && oSettings.oFeatures.bServerSide && 
+			     !_fnAjaxUpdate( oSettings ) )
+			{
+				return;
+			}
+			else if ( !oSettings.oFeatures.bServerSide )
+			{
+				oSettings.iDraw++;
+			}
+			
+			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] ];
+					var nRow = aoData.nTr;
+					
+					/* Remove the old stripping classes and then add the new one */
+					if ( iStrips !== 0 )
+					{
+						var sStrip = oSettings.asStripClasses[ iRowCount % iStrips ];
+						if ( aoData._sRowStripe != sStrip )
+						{
+							$(nRow).removeClass( aoData._sRowStripe ).addClass( sStrip );
+							aoData._sRowStripe = sStrip;
+						}
+					}
+					
+					/* Custom row callback function - might want to manipule the row */
+					if ( typeof oSettings.fnRowCallback == "function" )
+					{
+						nRow = oSettings.fnRowCallback.call( oSettings.oInstance, nRow, 
+							oSettings.aoData[ oSettings.aiDisplay[j] ]._aData, iRowCount, j );
+						if ( !nRow && !bRowError )
+						{
+							_fnLog( oSettings, 0, "A node was not returned by fnRowCallback" );
+							bRowError = true;
+						}
+					}
+					
+					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 );
+							}
+						}
+					}
+				}
+			}
+			else
+			{
+				/* Table is empty - create a row with an empty message in it */
+				anRows[ 0 ] = document.createElement( 'tr' );
+				
+				if ( typeof oSettings.asStripClasses[0] != 'undefined' )
+				{
+					anRows[ 0 ].className = oSettings.asStripClasses[0];
+				}
+				
+				var nTd = document.createElement( 'td' );
+				nTd.setAttribute( 'valign', "top" );
+				nTd.colSpan = _fnVisbleColumns( oSettings );
+				nTd.className = oSettings.oClasses.sRowEmpty;
+				if ( typeof oSettings.oLanguage.sEmptyTable != 'undefined' &&
+				     oSettings.fnRecordsTotal() === 0 )
+				{
+					nTd.innerHTML = oSettings.oLanguage.sEmptyTable;
+				}
+				else
+				{
+					nTd.innerHTML = oSettings.oLanguage.sZeroRecords.replace(
+						'_MAX_', oSettings.fnFormatNumber(oSettings.fnRecordsTotal()) );
+				}
+				
+				anRows[ iRowCount ].appendChild( nTd );
+			}
+			
+			/* Callback the header and footer custom funcation if there is one */
+			if ( typeof oSettings.fnHeaderCallback == 'function' )
+			{
+				oSettings.fnHeaderCallback.call( oSettings.oInstance, $('>tr', oSettings.nTHead)[0], 
+					_fnGetDataMaster( oSettings ), oSettings._iDisplayStart, oSettings.fnDisplayEnd(),
+					oSettings.aiDisplay );
+			}
+			
+			if ( typeof oSettings.fnFooterCallback == 'function' )
+			{
+				oSettings.fnFooterCallback.call( oSettings.oInstance, $('>tr', oSettings.nTFoot)[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 )
+				{
+					nTrs = oSettings.nTBody.childNodes;
+					for ( i=nTrs.length-1 ; i>=0 ; i-- )
+					{
+						nTrs[i].parentNode.removeChild( nTrs[i] );
+					}
+				}
+				
+				/* 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 */
+			for ( i=oSettings.aoDrawCallback.length-1 ; i>=0 ; i-- )
+			{
+				oSettings.aoDrawCallback[i].fn.call( oSettings.oInstance, 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 ( typeof oSettings._bInitComplete == 'undefined' )
+				{
+					_fnInitComplete( oSettings );
+				}
+			}
+		}
+		
+		/*
+		 * Function: _fnReDraw
+		 * Purpose:  Redraw the table - taking account of the various features which are enabled
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		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 );
+			}
+		}
+		
+		/*
+		 * Function: _fnAjaxUpdate
+		 * Purpose:  Update the table using an Ajax call
+		 * Returns:  bool: block the table drawing or not
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnAjaxUpdate( oSettings )
+		{
+			if ( oSettings.bAjaxDataGet )
+			{
+				_fnProcessingDisplay( oSettings, true );
+				var iColumns = oSettings.aoColumns.length;
+				var aoData = [];
+				var i;
+				
+				/* Paging and general */
+				oSettings.iDraw++;
+				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 } );
+				
+				/* 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 iFixed = oSettings.aaSortingFixed !== null ? oSettings.aaSortingFixed.length : 0;
+					var iUser = oSettings.aaSorting.length;
+					aoData.push( { "name": "iSortingCols",   "value": iFixed+iUser } );
+					for ( i=0 ; i<iFixed ; i++ )
+					{
+						aoData.push( { "name": "iSortCol_"+i,  "value": oSettings.aaSortingFixed[i][0] } );
+						aoData.push( { "name": "sSortDir_"+i,  "value": oSettings.aaSortingFixed[i][1] } );
+					}
+					
+					for ( i=0 ; i<iUser ; i++ )
+					{
+						aoData.push( { "name": "iSortCol_"+(i+iFixed),  "value": oSettings.aaSorting[i][0] } );
+						aoData.push( { "name": "sSortDir_"+(i+iFixed),  "value": oSettings.aaSorting[i][1] } );
+					}
+					
+					for ( i=0 ; i<iColumns ; i++ )
+					{
+						aoData.push( { "name": "bSortable_"+i,  "value": oSettings.aoColumns[i].bSortable } );
+					}
+				}
+				
+				oSettings.fnServerData.call( oSettings.oInstance, oSettings.sAjaxSource, aoData,
+					function(json) {
+						_fnAjaxUpdateDraw( oSettings, json );
+					} );
+				return false;
+			}
+			else
+			{
+				return true;
+			}
+		}
+		
+		/*
+		 * Function: _fnAjaxUpdateDraw
+		 * Purpose:  Data the data from the server (nuking the old) and redraw the table
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           object:json - json data return from the server.
+		 *             The following must be defined:
+		 *               iTotalRecords, iTotalDisplayRecords, aaData
+		 *             The following may be defined:
+		 *               sColumns
+		 */
+		function _fnAjaxUpdateDraw ( oSettings, json )
+		{
+			if ( typeof json.sEcho != 'undefined' )
+			{
+				/* Protect against old returns over-writing a new one. Possible when you get
+				 * very fast interaction, and later queires 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 = json.iTotalRecords;
+			oSettings._iRecordsDisplay = json.iTotalDisplayRecords;
+			
+			/* Determine if reordering is required */
+			var sOrdering = _fnColumnOrdering(oSettings);
+			var bReOrder = (typeof json.sColumns != 'undefined' && sOrdering !== "" && json.sColumns != sOrdering );
+			if ( bReOrder )
+			{
+				var aiIndex = _fnReOrderIndex( oSettings, json.sColumns );
+			}
+			
+			for ( var i=0, iLen=json.aaData.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 aData = [];
+					for ( var j=0, jLen=oSettings.aoColumns.length ; j<jLen ; j++ )
+					{
+						aData.push( json.aaData[i][ aiIndex[j] ] );
+					}
+					_fnAddData( oSettings, aData );
+				}
+				else
+				{
+					/* No re-order required, sever got it "right" - just straight add */
+					_fnAddData( oSettings, json.aaData[i] );
+				}
+			}
+			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+			
+			oSettings.bAjaxDataGet = false;
+			_fnDraw( oSettings );
+			oSettings.bAjaxDataGet = true;
+			_fnProcessingDisplay( oSettings, false );
+		}
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - Options (features) HTML
+		 */
+		
+		/*
+		 * Function: _fnAddOptionsHtml
+		 * Purpose:  Add the options to the page HTML for the table
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		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 = document.createElement( 'div' );
+			oSettings.nTable.parentNode.insertBefore( nHolding, oSettings.nTable );
+			
+			/* 
+			 * All DataTables are wrapped in a div - this is not currently optional - backwards 
+			 * compatability. It can be removed if you don't want it.
+			 */
+			oSettings.nTableWrapper = document.createElement( 'div' );
+			oSettings.nTableWrapper.className = oSettings.oClasses.sWrapper;
+			if ( oSettings.sTableId !== '' )
+			{
+				oSettings.nTableWrapper.setAttribute( 'id', oSettings.sTableId+'_wrapper' );
+			}
+			
+			/* 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 = document.createElement( 'div' );
+					
+					/* 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 = "fg-toolbar ui-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix";
+						}
+						else if ( sAttr == "F" )
+						{
+							sAttr = "fg-toolbar ui-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix";
+						}
+						
+						/* 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.setAttribute('id', aSplit[0].substr(1, aSplit[0].length-1) );
+							nNewNode.className = aSplit[1];
+						}
+						else if ( sAttr.charAt(0) == "#" )
+						{
+							nNewNode.setAttribute('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 ( _oExt.aoFeatures.length !== 0 )
+				{
+					/* Plug-in features */
+					var aoFeatures = _oExt.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 );
+		}
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - Feature: Filtering
+		 */
+		
+		/*
+		 * Function: _fnFeatureHtmlTable
+		 * Purpose:  Add any control elements for the table - specifically scrolling
+		 * Returns:  node: - Node to add to the DOM
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnFeatureHtmlTable ( oSettings )
+		{
+			/* Chack 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 = (typeof oInit.bJQueryUI != 'undefined' && oInit.bJQueryUI) ?
+			 		_oExt.oJUIClasses : _oExt.oStdClasses;
+			
+			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 = "150%"; /* 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 any caption elements from the body to the header */
+			var nCaptions = $('>caption', oSettings.nTable);
+			for ( var i=0, iLen=nCaptions.length ; i<iLen ; i++ )
+			{
+				nScrollHeadTable.appendChild( nCaptions[i] );
+			}
+			
+			/*
+			 * Sizing
+			 */
+			/* When xscrolling 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 )
+					{
+						/* 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;
+		}
+		
+		/*
+		 * Function: _fnScrollDraw
+		 * Purpose:  Update the various tables for resizing
+		 * Returns:  node: - Node to add to the DOM
+		 * Inputs:   object:o - dataTables settings object
+		 * Notes:    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
+		 */
+		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,
+				iWidth, aApplied=[], iSanityWidth;
+			
+			/*
+			 * 1. Re-create the table inside the scrolling div
+			 */
+			
+			/* Remove the old minimised thead and tfoot elements in the inner table */
+			var nTheadSize = o.nTable.getElementsByTagName('thead');
+			if ( nTheadSize.length > 0 )
+			{
+				o.nTable.removeChild( nTheadSize[0] );
+			}
+			
+			if ( o.nTFoot !== null )
+			{
+				/* Remove the old minimised footer element in the cloned header */
+				var nTfootSize = o.nTable.getElementsByTagName('tfoot');
+				if ( nTfootSize.length > 0 )
+				{
+					o.nTable.removeChild( nTfootSize[0] );
+				}
+			}
+			
+			/* Clone the current header and footer elements and then place it into the inner table */
+			nTheadSize = o.nTHead.cloneNode(true);
+			o.nTable.insertBefore( nTheadSize, o.nTable.childNodes[0] );
+			
+			if ( o.nTFoot !== null )
+			{
+				nTfootSize = o.nTFoot.cloneNode(true);
+				o.nTable.insertBefore( nTfootSize, o.nTable.childNodes[1] );
+			}
+			
+			/*
+			 * 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
+			 * calclated sizes to this header
+			 */
+			var nThs = _fnGetUniqueThs( 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 = "";
+				}, nTfootSize.getElementsByTagName('tr') );
+			}
+			
+			/* 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. This needs feature detection in future - to do
+				 */
+				if ( $.browser.msie && $.browser.version <= 7 )
+				{
+					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
+			 */
+			anHeadToSize = o.nTHead.getElementsByTagName('tr');
+			anHeadSizers = nTheadSize.getElementsByTagName('tr');
+			
+			_fnApplyToChildren( function(nSizer, nToSize) {
+				oStyle = nSizer.style;
+				oStyle.paddingTop = "0";
+				oStyle.paddingBottom = "0";
+				oStyle.borderTopWidth = "0";
+				oStyle.borderBottomWidth = "0";
+				oStyle.height = 0;
+				
+				iWidth = $(nSizer).width();
+				nToSize.style.width = _fnStringToCss( iWidth );
+				aApplied.push( iWidth );
+			}, anHeadSizers, anHeadToSize );
+			$(anHeadSizers).height(0);
+			
+			if ( o.nTFoot !== null )
+			{
+				/* Clone the current footer and then place it into the body table as a "hidden header" */
+				anFootSizers = nTfootSize.getElementsByTagName('tr');
+				anFootToSize = o.nTFoot.getElementsByTagName('tr');
+				
+				_fnApplyToChildren( function(nSizer, nToSize) {
+					oStyle = nSizer.style;
+					oStyle.paddingTop = "0";
+					oStyle.paddingBottom = "0";
+					oStyle.borderTopWidth = "0";
+					oStyle.borderBottomWidth = "0";
+					oStyle.height = 0;
+					
+					iWidth = $(nSizer).width();
+					nToSize.style.width = _fnStringToCss( iWidth );
+					aApplied.push( iWidth );
+				}, anFootSizers, 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) {
+				nSizer.innerHTML = "";
+				nSizer.style.width = _fnStringToCss( aApplied.shift() );
+			}, anHeadSizers );
+			
+			if ( o.nTFoot !== null )
+			{
+				_fnApplyToChildren( function(nSizer) {
+					nSizer.innerHTML = "";
+					nSizer.style.width = _fnStringToCss( aApplied.shift() );
+				}, anFootSizers );
+			}
+			
+			/* Sanity check that the table is of a sensible width. If not then we are going to get
+			 * misalignment
+			 */
+			if ( $(o.nTable).outerWidth() < iSanityWidth )
+			{
+				if ( o.oScroll.sX === "" )
+				{
+					_fnLog( o, 1, "The table cannot fit into the current element which will cause column"+
+						" misalignment. It is suggested that you enable x-scrolling or increase the width"+
+						" the table has in which to be drawn" );
+				}
+				else if ( o.oScroll.sXInner !== "" )
+				{
+					_fnLog( o, 1, "The table cannot fit into the current element which will cause column"+
+						" misalignment. It is suggested that you increase the sScrollXInner property to"+
+						" allow it to draw in a larger area, or simply remove that parameter to allow"+
+						" automatic calculation" );
+				}
+			}
+			
+			
+			/*
+			 * 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 ( $.browser.msie && $.browser.version <= 7 )
+				{
+					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).height()+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+o.oScroll.iBarWidth );
+			
+			if ( o.nTFoot !== null )
+			{
+				var
+					nScrollFootInner = o.nScrollFoot.getElementsByTagName('div')[0],
+					nScrollFootTable = nScrollFootInner.getElementsByTagName('table')[0];
+				
+				nScrollFootInner.style.width = _fnStringToCss( o.nTable.offsetWidth+o.oScroll.iBarWidth );
+				nScrollFootTable.style.width = _fnStringToCss( o.nTable.offsetWidth );
+			}
+			
+			/* If sorting or filtering has occured, jump the scrolling back to the top */
+			if ( o.bSorted || o.bFiltered )
+			{
+				nScrollBody.scrollTop = 0;
+			}
+		}
+		
+		/*
+		 * Function: _fnAjustColumnSizing
+		 * Purpose:  Ajust the table column widths for new data
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 * Notes:    You would probably want to do a redraw after calling this function!
+		 */
+		function _fnAjustColumnSizing ( oSettings )
+		{
+			/* Not interested in doing column width calculation if autowidth 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;
+			}
+		}
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - Feature: Filtering
+		 */
+		
+		/*
+		 * Function: _fnFeatureHtmlFilter
+		 * Purpose:  Generate the node required for filtering text
+		 * Returns:  node
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnFeatureHtmlFilter ( oSettings )
+		{
+			var nFilter = document.createElement( 'div' );
+			if ( oSettings.sTableId !== '' && typeof oSettings.aanFeatures.f == "undefined" )
+			{
+				nFilter.setAttribute( 'id', oSettings.sTableId+'_filter' );
+			}
+			nFilter.className = oSettings.oClasses.sFilter;
+			var sSpace = oSettings.oLanguage.sSearch==="" ? "" : " ";
+			nFilter.innerHTML = oSettings.oLanguage.sSearch+sSpace+'<input type="text" />';
+			
+			var jqFilter = $("input", nFilter);
+			jqFilter.val( oSettings.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;
+				for ( var i=0, iLen=n.length ; i<iLen ; i++ )
+				{
+					if ( n[i] != this.parentNode )
+					{
+						$('input', n[i]).val( this.value );
+					}
+				}
+				
+				/* Now do the filter */
+				if ( this.value != oSettings.oPreviousSearch.sSearch )
+				{
+					_fnFilterComplete( oSettings, { 
+						"sSearch": this.value, 
+						"bRegex":  oSettings.oPreviousSearch.bRegex,
+						"bSmart":  oSettings.oPreviousSearch.bSmart 
+					} );
+				}
+			} );
+			
+			jqFilter.bind( 'keypress.DT', function(e) {
+				/* Prevent default */
+				if ( e.keyCode == 13 )
+				{
+					return false;
+				}
+			} );
+			
+			return nFilter;
+		}
+		
+		/*
+		 * Function: _fnFilterComplete
+		 * Purpose:  Filter the table using both the global filter and column based filtering
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           object:oSearch: search information
+		 *           int:iForce - optional - force a research of the master array (1) or not (undefined or 0)
+		 */
+		function _fnFilterComplete ( oSettings, oInput, iForce )
+		{
+			/* Filter on everything */
+			_fnFilter( oSettings, oInput.sSearch, iForce, oInput.bRegex, oInput.bSmart );
+			
+			/* Now do the individual column filter */
+			for ( var i=0 ; i<oSettings.aoPreSearchCols.length ; i++ )
+			{
+				_fnFilterColumn( oSettings, oSettings.aoPreSearchCols[i].sSearch, i, 
+					oSettings.aoPreSearchCols[i].bRegex, oSettings.aoPreSearchCols[i].bSmart );
+			}
+			
+			/* Custom filtering */
+			if ( _oExt.afnFiltering.length !== 0 )
+			{
+				_fnFilterCustom( oSettings );
+			}
+			
+			/* Tell the draw function we have been filtering */
+			oSettings.bFiltered = true;
+			
+			/* Redraw the table */
+			oSettings._iDisplayStart = 0;
+			_fnCalculateEnd( oSettings );
+			_fnDraw( oSettings );
+			
+			/* Rebuild search array 'offline' */
+			_fnBuildSearchArray( oSettings, 0 );
+		}
+		
+		/*
+		 * Function: _fnFilterCustom
+		 * Purpose:  Apply custom filtering functions
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnFilterCustom( oSettings )
+		{
+			var afnFilters = _oExt.afnFiltering;
+			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];
+					
+					/* Check if we should use this row based on the filtering function */
+					if ( !afnFilters[i]( oSettings, oSettings.aoData[iDisIndex]._aData, iDisIndex ) )
+					{
+						oSettings.aiDisplay.splice( j-iCorrector, 1 );
+						iCorrector++;
+					}
+				}
+			}
+		}
+		
+		/*
+		 * Function: _fnFilterColumn
+		 * Purpose:  Filter the table on a per-column basis
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           string:sInput - string to filter on
+		 *           int:iColumn - column to filter
+		 *           bool:bRegex - treat search string as a regular expression or not
+		 *           bool:bSmart - use smart filtering or not
+		 */
+		function _fnFilterColumn ( oSettings, sInput, iColumn, bRegex, bSmart )
+		{
+			if ( sInput === "" )
+			{
+				return;
+			}
+			
+			var iIndexCorrector = 0;
+			var rpSearch = _fnFilterCreateSearch( sInput, bRegex, bSmart );
+			
+			for ( var i=oSettings.aiDisplay.length-1 ; i>=0 ; i-- )
+			{
+				var sData = _fnDataToSearch( oSettings.aoData[ oSettings.aiDisplay[i] ]._aData[iColumn],
+					oSettings.aoColumns[iColumn].sType );
+				if ( ! rpSearch.test( sData ) )
+				{
+					oSettings.aiDisplay.splice( i, 1 );
+					iIndexCorrector++;
+				}
+			}
+		}
+		
+		/*
+		 * Function: _fnFilter
+		 * Purpose:  Filter the data table based on user input and draw the table
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           string:sInput - string to filter on
+		 *           int:iForce - optional - force a research of the master array (1) or not (undefined or 0)
+		 *           bool:bRegex - treat as a regular expression or not
+		 *           bool:bSmart - perform smart filtering or not
+		 */
+		function _fnFilter( oSettings, sInput, iForce, bRegex, bSmart )
+		{
+			var i;
+			var rpSearch = _fnFilterCreateSearch( sInput, bRegex, bSmart );
+			
+			/* Check if we are forcing or not - optional parameter */
+			if ( typeof iForce == 'undefined' || iForce === null )
+			{
+				iForce = 0;
+			}
+			
+			/* Need to take account of custom filtering functions - always filter */
+			if ( _oExt.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 ||
+					   oSettings.oPreviousSearch.sSearch.length > sInput.length || iForce == 1 ||
+					   sInput.indexOf(oSettings.oPreviousSearch.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++;
+			  		}
+			  	}
+			  }
+			}
+			oSettings.oPreviousSearch.sSearch = sInput;
+			oSettings.oPreviousSearch.bRegex = bRegex;
+			oSettings.oPreviousSearch.bSmart = bSmart;
+		}
+		
+		/*
+		 * Function: _fnBuildSearchArray
+		 * Purpose:  Create an array which can be quickly search through
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           int:iMaster - use the master data array - optional
+		 */
+		function _fnBuildSearchArray ( oSettings, iMaster )
+		{
+			/* Clear out the old data */
+			oSettings.asDataSearch.splice( 0, oSettings.asDataSearch.length );
+			
+			var aArray = (typeof iMaster != 'undefined' && iMaster == 1) ?
+			 	oSettings.aiDisplayMaster : oSettings.aiDisplay;
+			
+			for ( var i=0, iLen=aArray.length ; i<iLen ; i++ )
+			{
+				oSettings.asDataSearch[i] = _fnBuildSearchRow( oSettings, 
+					oSettings.aoData[ aArray[i] ]._aData );
+			}
+		}
+		
+		/*
+		 * Function: _fnBuildSearchRow
+		 * Purpose:  Create a searchable string from a single data row
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           array:aData - aoData[]._aData array to use for the data to search
+		 */
+		function _fnBuildSearchRow( oSettings, aData )
+		{
+			var sSearch = '';
+			var nTmp = document.createElement('div');
+			
+			for ( var j=0, jLen=oSettings.aoColumns.length ; j<jLen ; j++ )
+			{
+				if ( oSettings.aoColumns[j].bSearchable )
+				{
+					var sData = aData[j];
+					sSearch += _fnDataToSearch( sData, oSettings.aoColumns[j].sType )+'  ';
+				}
+			}
+			
+			/* If it looks like there is an HTML entity in the string, attempt to decode it */
+			if ( sSearch.indexOf('&') !== -1 )
+			{
+				nTmp.innerHTML = sSearch;
+				sSearch = nTmp.textContent ? nTmp.textContent : nTmp.innerText;
+				
+				/* IE and Opera appear to put an newline where there is a <br> tag - remove it */
+				sSearch = sSearch.replace(/\n/g," ").replace(/\r/g,"");
+			}
+			
+			return sSearch;
+		}
+		
+		/*
+		 * Function: _fnFilterCreateSearch
+		 * Purpose:  Build a regular expression object suitable for searching a table
+		 * Returns:  RegExp: - constructed object
+		 * Inputs:   string:sSearch - string to search for
+		 *           bool:bRegex - treat as a regular expression or not
+		 *           bool:bSmart - perform smart filtering or not
+		 */
+		function _fnFilterCreateSearch( sSearch, bRegex, bSmart )
+		{
+			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, "i" );
+			}
+			else
+			{
+				sSearch = bRegex ? sSearch : _fnEscapeRegex( sSearch );
+				return new RegExp( sSearch, "i" );
+			}
+		}
+		
+		/*
+		 * Function: _fnDataToSearch
+		 * Purpose:  Convert raw data into something that the user can search on
+		 * Returns:  string: - search string
+		 * Inputs:   string:sData - data to be modified
+		 *           string:sType - data type
+		 */
+		function _fnDataToSearch ( sData, sType )
+		{
+			if ( typeof _oExt.ofnSearch[sType] == "function" )
+			{
+				return _oExt.ofnSearch[sType]( sData );
+			}
+			else if ( sType == "html" )
+			{
+				return sData.replace(/\n/g," ").replace( /<.*?>/g, "" );
+			}
+			else if ( typeof sData == "string" )
+			{
+				return sData.replace(/\n/g," ");
+			}
+			return sData;
+		}
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - Feature: Sorting
+		 */
+		
+		/*
+	 	 * Function: _fnSort
+		 * Purpose:  Change the order of the table
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           bool:bApplyClasses - optional - should we apply classes or not
+		 * Notes:    We always sort the master array and then apply a filter again
+		 *   if it is needed. This probably isn't optimal - but atm I can't think
+		 *   of any other way which is (each has disadvantages). we want to sort aiDisplayMaster - 
+		 *   but according to aoData[]._aData
+		 */
+		function _fnSort ( oSettings, bApplyClasses )
+		{
+			var
+				iDataSort, iDataType,
+				i, iLen, j, jLen,
+				aaSort = [],
+			 	aiOrig = [],
+				oSort = _oExt.oSort,
+				aoData = oSettings.aoData,
+				aoColumns = oSettings.aoColumns;
+			
+			/* No sorting required if server-side or no sorting array */
+			if ( !oSettings.oFeatures.bServerSide && 
+				(oSettings.aaSorting.length !== 0 || oSettings.aaSortingFixed !== null) )
+			{
+				if ( oSettings.aaSortingFixed !== null )
+				{
+					aaSort = oSettings.aaSortingFixed.concat( oSettings.aaSorting );
+				}
+				else
+				{
+					aaSort = oSettings.aaSorting.slice();
+				}
+				
+				/* If there is a sorting data type, and a fuction 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 );
+					var sDataType = oSettings.aoColumns[ iColumn ].sSortDataType;
+					if ( typeof _oExt.afnSortData[sDataType] != 'undefined' )
+					{
+						var aData = _oExt.afnSortData[sDataType]( oSettings, iColumn, iVisColumn );
+						for ( j=0, jLen=aoData.length ; j<jLen ; j++ )
+						{
+							aoData[j]._aData[iColumn] = aData[j];
+						}
+					}
+				}
+				
+				/* 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;
+				}
+				
+				/* 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.
+				 */
+				var iSortLen = aaSort.length;
+				oSettings.aiDisplayMaster.sort( function ( a, b ) {
+					var iTest;
+					for ( i=0 ; i<iSortLen ; i++ )
+					{
+						iDataSort = aoColumns[ aaSort[i][0] ].iDataSort;
+						iDataType = aoColumns[ iDataSort ].sType;
+						iTest = oSort[ iDataType+"-"+aaSort[i][1] ](
+							aoData[a]._aData[iDataSort],
+							aoData[b]._aData[iDataSort]
+						);
+						
+						if ( iTest !== 0 )
+						{
+							return iTest;
+						}
+					}
+					
+					return oSort['numeric-asc']( aiOrig[a], aiOrig[b] );
+				} );
+			}
+			
+			/* Alter the sorting classes to take account of the changes */
+			if ( typeof bApplyClasses == 'undefined' || bApplyClasses )
+			{
+				_fnSortingClasses( oSettings );
+			}
+			
+			/* Tell the draw function that we have sorted the data */
+			oSettings.bSorted = true;
+			
+			/* 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 );
+			}
+		}
+		
+		/*
+		 * Function: _fnSortAttachListener
+		 * Purpose:  Attach a sort handler (click) to a node
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           node:nNode - node to attach the handler to
+		 *           int:iDataIndex - column sorting index
+		 *           function:fnCallback - callback function - optional
+		 */
+		function _fnSortAttachListener ( oSettings, nNode, iDataIndex, fnCallback )
+		{
+			$(nNode).bind( 'click.DT', 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
+				 * _fnDrawHead 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 excuting
+				 * 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 on;y do it when we absolutely have to.
+				 */
+				var fnInnerSorting = function () {
+					var iColumn, iNextSort;
+					
+					/* If the shift key is pressed then we are multipe 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 ( typeof oSettings.aoColumns[iColumn].asSorting[iNextSort] == 'undefined' )
+								{
+									/* 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 ( typeof oSettings.aoColumns[iColumn].asSorting[iNextSort] == 'undefined' )
+							{
+								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 );
+				}
+			} );
+		}
+		
+		/*
+		 * Function: _fnSortingClasses
+		 * Purpose:  Set the sortting classes on the header
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 * Notes:    It is safe to call this function when bSort and bSortClasses are false
+		 */
+		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", 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 admitadly fairly ugly. It could be made a lot 
+			 * simpiler using jQuery selectors and add/removeClass, but that is significantly slower
+			 * (on the order of 5 times slower) - hence the direct DOM manipulation here.
+			 */
+			sClass = oClasses.sSortColumn;
+			
+			if ( oSettings.oFeatures.bSort && oSettings.oFeatures.bSortClasses )
+			{
+				var nTds = _fnGetTdNodes( oSettings );
+				
+				/* Remove the old classes */
+				if ( nTds.length >= iColumns )
+				{
+					for ( i=0 ; i<iColumns ; i++ )
+					{
+						if ( nTds[i].className.indexOf(sClass+"1") != -1 )
+						{
+							for ( j=0, jLen=(nTds.length/iColumns) ; j<jLen ; j++ )
+							{
+								nTds[(iColumns*j)+i].className = 
+									$.trim( nTds[(iColumns*j)+i].className.replace( sClass+"1", "" ) );
+							}
+						}
+						else if ( nTds[i].className.indexOf(sClass+"2") != -1 )
+						{
+							for ( j=0, jLen=(nTds.length/iColumns) ; j<jLen ; j++ )
+							{
+								nTds[(iColumns*j)+i].className = 
+									$.trim( nTds[(iColumns*j)+i].className.replace( sClass+"2", "" ) );
+							}
+						}
+						else if ( nTds[i].className.indexOf(sClass+"3") != -1 )
+						{
+							for ( j=0, jLen=(nTds.length/iColumns) ; j<jLen ; j++ )
+							{
+								nTds[(iColumns*j)+i].className = 
+									$.trim( nTds[(iColumns*j)+i].className.replace( " "+sClass+"3", "" ) );
+							}
+						}
+					}
+				}
+				
+				/* Add the new classes to the table */
+				var iClass = 1, iTargetCol;
+				for ( i=0 ; i<aaSort.length ; i++ )
+				{
+					iTargetCol = parseInt( aaSort[i][0], 10 );
+					for ( j=0, jLen=(nTds.length/iColumns) ; j<jLen ; j++ )
+					{
+						nTds[(iColumns*j)+iTargetCol].className += " "+sClass+iClass;
+					}
+					
+					if ( iClass < 3 )
+					{
+						iClass++;
+					}
+				}
+			}
+		}
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - Feature: Pagination. Note that most of the paging logic is done in 
+		 * _oExt.oPagination
+		 */
+		
+		/*
+		 * Function: _fnFeatureHtmlPaginate
+		 * Purpose:  Generate the node required for default pagination
+		 * Returns:  node
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnFeatureHtmlPaginate ( oSettings )
+		{
+			if ( oSettings.oScroll.bInfinite )
+			{
+				return null;
+			}
+			
+			var nPaginate = document.createElement( 'div' );
+			nPaginate.className = oSettings.oClasses.sPaging+oSettings.sPaginationType;
+			
+			_oExt.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 ( typeof oSettings.aanFeatures.p == "undefined" )
+			{
+				oSettings.aoDrawCallback.push( {
+					"fn": function( oSettings ) {
+						_oExt.oPagination[ oSettings.sPaginationType ].fnUpdate( oSettings, function( oSettings ) {
+							_fnCalculateEnd( oSettings );
+							_fnDraw( oSettings );
+						} );
+					},
+					"sName": "pagination"
+				} );
+			}
+			return nPaginate;
+		}
+		
+		/*
+		 * Function: _fnPageChange
+		 * Purpose:  Alter the display settings to change the page
+		 * Returns:  bool:true - page has changed, false - no change (no effect) eg 'first' on page 1
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           string:sAction - paging action to take: "first", "previous", "next" or "last"
+		 */
+		function _fnPageChange ( oSettings, sAction )
+		{
+			var iOldStart = oSettings._iDisplayStart;
+			
+			if ( sAction == "first" )
+			{
+				oSettings._iDisplayStart = 0;
+			}
+			else if ( sAction == "previous" )
+			{
+				oSettings._iDisplayStart = oSettings._iDisplayLength>=0 ?
+					oSettings._iDisplayStart - oSettings._iDisplayLength :
+					0;
+				
+				/* Correct for underrun */
+				if ( oSettings._iDisplayStart < 0 )
+				{
+				  oSettings._iDisplayStart = 0;
+				}
+			}
+			else if ( sAction == "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 ( sAction == "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: "+sAction );
+			}
+			
+			return iOldStart != oSettings._iDisplayStart;
+		}
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - Feature: HTML info
+		 */
+		
+		/*
+		 * Function: _fnFeatureHtmlInfo
+		 * Purpose:  Generate the node required for the info display
+		 * Returns:  node
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		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 ( typeof oSettings.aanFeatures.i == "undefined" )
+			{
+				/* Add draw callback */
+				oSettings.aoDrawCallback.push( {
+					"fn": _fnUpdateInfo,
+					"sName": "information"
+				} );
+				
+				/* Add id */
+				if ( oSettings.sTableId !== '' )
+				{
+					nInfo.setAttribute( 'id', oSettings.sTableId+'_info' );
+				}
+			}
+			
+			return nInfo;
+		}
+		
+		/*
+		 * Function: _fnUpdateInfo
+		 * Purpose:  Update the information elements in the display
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnUpdateInfo ( oSettings )
+		{
+			/* Show information about the table */
+			if ( !oSettings.oFeatures.bInfo || oSettings.aanFeatures.i.length === 0 )
+			{
+				return;
+			}
+			
+			var
+				iStart = oSettings._iDisplayStart+1, iEnd = oSettings.fnDisplayEnd(),
+				iMax = oSettings.fnRecordsTotal(), iTotal = oSettings.fnRecordsDisplay(),
+				sStart = oSettings.fnFormatNumber( iStart ), sEnd = oSettings.fnFormatNumber( iEnd ),
+				sMax = oSettings.fnFormatNumber( iMax ), sTotal = oSettings.fnFormatNumber( iTotal ),
+				sOut;
+			
+			/* When infinite scrolling, we are always starting at 1. _iDisplayStart is used only
+			 * internally
+			 */
+			if ( oSettings.oScroll.bInfinite )
+			{
+				sStart = oSettings.fnFormatNumber( 1 );
+			}
+			
+			if ( oSettings.fnRecordsDisplay() === 0 && 
+				   oSettings.fnRecordsDisplay() == oSettings.fnRecordsTotal() )
+			{
+				/* Empty record set */
+				sOut = oSettings.oLanguage.sInfoEmpty+ oSettings.oLanguage.sInfoPostFix;
+			}
+			else if ( oSettings.fnRecordsDisplay() === 0 )
+			{
+				/* Rmpty record set after filtering */
+				sOut = oSettings.oLanguage.sInfoEmpty +' '+ 
+					oSettings.oLanguage.sInfoFiltered.replace('_MAX_', sMax)+
+						oSettings.oLanguage.sInfoPostFix;
+			}
+			else if ( oSettings.fnRecordsDisplay() == oSettings.fnRecordsTotal() )
+			{
+				/* Normal record set */
+				sOut = oSettings.oLanguage.sInfo.
+						replace('_START_', sStart).
+						replace('_END_',   sEnd).
+						replace('_TOTAL_', sTotal)+ 
+					oSettings.oLanguage.sInfoPostFix;
+			}
+			else
+			{
+				/* Record set after filtering */
+				sOut = oSettings.oLanguage.sInfo.
+						replace('_START_', sStart).
+						replace('_END_',   sEnd).
+						replace('_TOTAL_', sTotal) +' '+ 
+					oSettings.oLanguage.sInfoFiltered.replace('_MAX_', 
+						oSettings.fnFormatNumber(oSettings.fnRecordsTotal()))+ 
+					oSettings.oLanguage.sInfoPostFix;
+			}
+			
+			if ( oSettings.oLanguage.fnInfoCallback !== null )
+			{
+				sOut = oSettings.oLanguage.fnInfoCallback( 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 );
+			}
+		}
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - Feature: Length change
+		 */
+		
+		/*
+		 * Function: _fnFeatureHtmlLength
+		 * Purpose:  Generate the node required for user display length changing
+		 * Returns:  node
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		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 = (oSettings.sTableId === "") ? "" : 'name="'+oSettings.sTableId+'_length"';
+			var sStdMenu = '<select size="1" '+sName+'>';
+			var i, iLen;
+			
+			if ( oSettings.aLengthMenu.length == 2 && typeof oSettings.aLengthMenu[0] == 'object' && 
+					typeof oSettings.aLengthMenu[1] == 'object' )
+			{
+				for ( i=0, iLen=oSettings.aLengthMenu[0].length ; i<iLen ; i++ )
+				{
+					sStdMenu += '<option value="'+oSettings.aLengthMenu[0][i]+'">'+
+						oSettings.aLengthMenu[1][i]+'</option>';
+				}
+			}
+			else
+			{
+				for ( i=0, iLen=oSettings.aLengthMenu.length ; i<iLen ; i++ )
+				{
+					sStdMenu += '<option value="'+oSettings.aLengthMenu[i]+'">'+
+						oSettings.aLengthMenu[i]+'</option>';
+				}
+			}
+			sStdMenu += '</select>';
+			
+			var nLength = document.createElement( 'div' );
+			if ( oSettings.sTableId !== '' && typeof oSettings.aanFeatures.l == "undefined" )
+			{
+				nLength.setAttribute( 'id', oSettings.sTableId+'_length' );
+			}
+			nLength.className = oSettings.oClasses.sLength;
+			nLength.innerHTML = oSettings.oLanguage.sLengthMenu.replace( '_MENU_', sStdMenu );
+			
+			/*
+			 * 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 );
+			} );
+			
+			return nLength;
+		}
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - Feature: Processing incidator
+		 */
+		
+		/*
+		 * Function: _fnFeatureHtmlProcessing
+		 * Purpose:  Generate the node required for the processing node
+		 * Returns:  node
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnFeatureHtmlProcessing ( oSettings )
+		{
+			var nProcessing = document.createElement( 'div' );
+			
+			if ( oSettings.sTableId !== '' && typeof oSettings.aanFeatures.r == "undefined" )
+			{
+				nProcessing.setAttribute( 'id', oSettings.sTableId+'_processing' );
+			}
+			nProcessing.innerHTML = oSettings.oLanguage.sProcessing;
+			nProcessing.className = oSettings.oClasses.sProcessing;
+			oSettings.nTable.parentNode.insertBefore( nProcessing, oSettings.nTable );
+			
+			return nProcessing;
+		}
+		
+		/*
+		 * Function: _fnProcessingDisplay
+		 * Purpose:  Display or hide the processing indicator
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           bool:
+		 *   true - show the processing indicator
+		 *   false - don't show
+		 */
+		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";
+				}
+			}
+		}
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - Support functions
+		 */
+		
+		/*
+		 * Function: _fnVisibleToColumnIndex
+		 * Purpose:  Covert the index of a visible column to the index in the data array (take account
+		 *   of hidden columns)
+		 * Returns:  int:i - the data index
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnVisibleToColumnIndex( oSettings, iMatch )
+		{
+			var iColumn = -1;
+			
+			for ( var i=0 ; i<oSettings.aoColumns.length ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bVisible === true )
+				{
+					iColumn++;
+				}
+				
+				if ( iColumn == iMatch )
+				{
+					return i;
+				}
+			}
+			
+			return null;
+		}
+		
+		/*
+		 * Function: _fnColumnIndexToVisible
+		 * Purpose:  Covert the index of an index in the data array and convert it to the visible
+		 *   column index (take account of hidden columns)
+		 * Returns:  int:i - the data index
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnColumnIndexToVisible( oSettings, iMatch )
+		{
+			var iVisible = -1;
+			for ( var i=0 ; i<oSettings.aoColumns.length ; i++ )
+			{
+				if ( oSettings.aoColumns[i].bVisible === true )
+				{
+					iVisible++;
+				}
+				
+				if ( i == iMatch )
+				{
+					return oSettings.aoColumns[i].bVisible === true ? iVisible : null;
+				}
+			}
+			
+			return null;
+		}
+		
+		
+		/*
+		 * Function: _fnNodeToDataIndex
+		 * Purpose:  Take a TR element and convert it to an index in aoData
+		 * Returns:  int:i - index if found, null if not
+		 * Inputs:   object:s - dataTables settings object
+		 *           node:n - the TR element to find
+		 */
+		function _fnNodeToDataIndex( s, n )
+		{
+			var i, iLen;
+			
+			/* Optimisation - see if the nodes which are currently visible match, since that is
+			 * the most likely node to be asked for (a selector or event for example)
+			 */
+			for ( i=s._iDisplayStart, iLen=s._iDisplayEnd ; i<iLen ; i++ )
+			{
+				if ( s.aoData[ s.aiDisplay[i] ].nTr == n )
+				{
+					return s.aiDisplay[i];
+				}
+			}
+			
+			/* Otherwise we are in for a slog through the whole data cache */
+			for ( i=0, iLen=s.aoData.length ; i<iLen ; i++ )
+			{
+				if ( s.aoData[i].nTr == n )
+				{
+					return i;
+				}
+			}
+			return null;
+		}
+		
+		/*
+		 * Function: _fnVisbleColumns
+		 * Purpose:  Get the number of visible columns
+		 * Returns:  int:i - the number of visible columns
+		 * Inputs:   object:oS - dataTables settings object
+		 */
+		function _fnVisbleColumns( oS )
+		{
+			var iVis = 0;
+			for ( var i=0 ; i<oS.aoColumns.length ; i++ )
+			{
+				if ( oS.aoColumns[i].bVisible === true )
+				{
+					iVis++;
+				}
+			}
+			return iVis;
+		}
+		
+		/*
+		 * Function: _fnCalculateEnd
+		 * Purpose:  Rcalculate the end point based on the start point
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		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;
+				}
+			}
+		}
+		
+		/*
+		 * Function: _fnConvertToWidth
+		 * Purpose:  Convert a CSS unit width to pixels (e.g. 2em)
+		 * Returns:  int:iWidth - width in pixels
+		 * Inputs:   string:sWidth - width to be converted
+		 *           node:nParent - parent to get the with for (required for
+		 *             relative widths) - optional
+		 */
+		function _fnConvertToWidth ( sWidth, nParent )
+		{
+			if ( !sWidth || sWidth === null || sWidth === '' )
+			{
+				return 0;
+			}
+			
+			if ( typeof nParent == "undefined" )
+			{
+				nParent = document.getElementsByTagName('body')[0];
+			}
+			
+			var iWidth;
+			var nTmp = document.createElement( "div" );
+			nTmp.style.width = sWidth;
+			
+			nParent.appendChild( nTmp );
+			iWidth = nTmp.offsetWidth;
+			nParent.removeChild( nTmp );
+			
+			return ( iWidth );
+		}
+		
+		/*
+		 * Function: _fnCalculateColumnWidths
+		 * Purpose:  Calculate the width of columns for the table
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnCalculateColumnWidths ( oSettings )
+		{
+			var iTableWidth = oSettings.nTable.offsetWidth;
+			var iUserInputs = 0;
+			var iTmpWidth;
+			var iVisibleColumns = 0;
+			var iColums = oSettings.aoColumns.length;
+			var i;
+			var oHeaders = $('th', oSettings.nTHead);
+			
+			/* 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, 
+							oSettings.nTable.parentNode );
+						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 ),
+					nBody = document.createElement( 'tbody' ),
+					nTr = document.createElement( 'tr' ),
+					nDivSizing;
+				
+				nCalcTmp.removeAttribute( "id" );
+				nCalcTmp.appendChild( oSettings.nTHead.cloneNode(true) );
+				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);
+				}
+				jqColSizing.each( function (i) {
+					this.style.width = "";
+					
+					var iIndex = _fnVisibleToColumnIndex( oSettings, i );
+					if ( iIndex !== null && oSettings.aoColumns[iIndex].sWidthOrig !== "" )
+					{
+						this.style.width = oSettings.aoColumns[iIndex].sWidthOrig;
+					}
+				} );
+				
+				/* 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);
+							nTr.appendChild( nTd );
+						}
+					}
+				}
+				
+				/* Build the table and 'display' it */
+				var nWrapper = oSettings.nTable.parentNode;
+				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 );
+				}
+				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)>td", nCalcTmp);
+				if ( oNodes.length === 0 )
+				{
+					oNodes = $("thead tr:eq(0)>th", nCalcTmp);
+				}
+				
+				var iIndex, iCorrector = 0, iWidth;
+				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+				{
+					if ( oSettings.aoColumns[i].bVisible )
+					{
+						iWidth = $(oNodes[iCorrector]).outerWidth();
+						if ( iWidth !== null && iWidth > 0 )
+						{
+							oSettings.aoColumns[i].sWidth = _fnStringToCss( iWidth );
+						}
+						iCorrector++;
+					}
+				}
+				
+				oSettings.nTable.style.width = _fnStringToCss( $(nCalcTmp).outerWidth() );
+				nCalcTmp.parentNode.removeChild( nCalcTmp );
+			}
+		}
+		
+		/*
+		 * Function: _fnScrollingWidthAdjust
+		 * Purpose:  Adjust a table's width to take account of scrolling
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           node:n - table node
+		 */
+		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() );
+			}
+		}
+		
+		/*
+		 * Function: _fnGetWidestNode
+		 * Purpose:  Get the widest node
+		 * Returns:  string: - max strlens for each column
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           int:iCol - column of interest
+		 *           boolean:bFast - Should we use fast (but non-accurate) calculation - optional,
+		 *             default true
+		 * Notes:    This operation is _expensive_ (!!!). It requires a lot of DOM interaction, but
+		 *   this is the only way to reliably get the widest string. For example 'mmm' would be wider
+		 *   than 'iiii' so we can't just ocunt characters. If this can be optimised it would be good
+		 *   to do so!
+		 */
+		function _fnGetWidestNode( oSettings, iCol, bFast )
+		{
+			/* Use fast not non-accurate calculate based on the strlen */
+			if ( typeof bFast == 'undefined' || bFast )
+			{
+				var iMaxLen = _fnGetMaxLenString( oSettings, iCol );
+				var iFastVis = _fnColumnIndexToVisible( oSettings, iCol);
+				if ( iMaxLen < 0 )
+				{
+					return null;
+				}
+				return oSettings.aoData[iMaxLen].nTr.getElementsByTagName('td')[iFastVis];
+			}
+			
+			/* Use the slow approach, but get high quality answers - note that this code is not actually
+			 * used by DataTables by default. If you want to use it you can alter the call to 
+			 * _fnGetWidestNode to pass 'false' as the third argument
+			 */
+			var
+				iMax = -1, i, iLen,
+				iMaxIndex = -1,
+				n = document.createElement('div');
+			
+			n.style.visibility = "hidden";
+			n.style.position = "absolute";
+			document.body.appendChild( n );
+			
+			for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
+			{
+				n.innerHTML = oSettings.aoData[i]._aData[iCol];
+				if ( n.offsetWidth > iMax )
+				{
+					iMax = n.offsetWidth;
+					iMaxIndex = i;
+				}
+			}
+			document.body.removeChild( n );
+			
+			if ( iMaxIndex >= 0 )
+			{
+				var iVis = _fnColumnIndexToVisible( oSettings, iCol);
+				var nRet = oSettings.aoData[iMaxIndex].nTr.getElementsByTagName('td')[iVis];
+				if ( nRet )
+				{
+					return nRet;
+				}
+			}
+			return null;
+		}
+		
+		/*
+		 * Function: _fnGetMaxLenString
+		 * Purpose:  Get the maximum strlen for each data column
+		 * Returns:  string: - max strlens for each column
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           int:iCol - column of interest
+		 */
+		function _fnGetMaxLenString( oSettings, iCol )
+		{
+			var iMax = -1;
+			var iMaxIndex = -1;
+			
+			for ( var i=0 ; i<oSettings.aoData.length ; i++ )
+			{
+				var s = oSettings.aoData[i]._aData[iCol];
+				if ( s.length > iMax )
+				{
+					iMax = s.length;
+					iMaxIndex = i;
+				}
+			}
+			
+			return iMaxIndex;
+		}
+		
+		/*
+		 * Function: _fnStringToCss
+		 * Purpose:  Append a CSS unit (only if required) to a string
+		 * Returns:  0 if match, 1 if length is different, 2 if no match
+		 * Inputs:   array:aArray1 - first array
+		 *           array:aArray2 - second array
+		 */
+		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";
+		}
+		
+		/*
+		 * Function: _fnArrayCmp
+		 * Purpose:  Compare two arrays
+		 * Returns:  0 if match, 1 if length is different, 2 if no match
+		 * Inputs:   array:aArray1 - first array
+		 *           array:aArray2 - second array
+		 */
+		function _fnArrayCmp( aArray1, aArray2 )
+		{
+			if ( aArray1.length != aArray2.length )
+			{
+				return 1;
+			}
+			
+			for ( var i=0 ; i<aArray1.length ; i++ )
+			{
+				if ( aArray1[i] != aArray2[i] )
+				{
+					return 2;
+				}
+			}
+			
+			return 0;
+		}
+		
+		/*
+		 * Function: _fnDetectType
+		 * Purpose:  Get the sort type based on an input string
+		 * Returns:  string: - type (defaults to 'string' if no type can be detected)
+		 * Inputs:   string:sData - data we wish to know the type of
+		 * Notes:    This function makes use of the DataTables plugin objct _oExt 
+		 *   (.aTypes) such that new types can easily be added.
+		 */
+		function _fnDetectType( sData )
+		{
+			var aTypes = _oExt.aTypes;
+			var iLen = aTypes.length;
+			
+			for ( var i=0 ; i<iLen ; i++ )
+			{
+				var sType = aTypes[i]( sData );
+				if ( sType !== null )
+				{
+					return sType;
+				}
+			}
+			
+			return 'string';
+		}
+		
+		/*
+		 * Function: _fnSettingsFromNode
+		 * Purpose:  Return the settings object for a particular table
+		 * Returns:  object: Settings object - or null if not found
+		 * Inputs:   node:nTable - table we are using as a dataTable
+		 */
+		function _fnSettingsFromNode ( nTable )
+		{
+			for ( var i=0 ; i<_aoSettings.length ; i++ )
+			{
+				if ( _aoSettings[i].nTable == nTable )
+				{
+					return _aoSettings[i];
+				}
+			}
+			
+			return null;
+		}
+		
+		/*
+		 * Function: _fnGetDataMaster
+		 * Purpose:  Return an array with the full table data
+		 * Returns:  array array:aData - Master data array
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		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;
+		}
+		
+		/*
+		 * Function: _fnGetTrNodes
+		 * Purpose:  Return an array with the TR nodes for the table
+		 * Returns:  array: - TR array
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnGetTrNodes ( oSettings )
+		{
+			var aNodes = [];
+			var iLen = oSettings.aoData.length;
+			for ( var i=0 ; i<iLen ; i++ )
+			{
+				aNodes.push( oSettings.aoData[i].nTr );
+			}
+			return aNodes;
+		}
+		
+		/*
+		 * Function: _fnGetTdNodes
+		 * Purpose:  Return an array with the TD nodes for the table
+		 * Returns:  array: - TD array
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnGetTdNodes ( oSettings )
+		{
+			var nTrs = _fnGetTrNodes( oSettings );
+			var nTds = [], nTd;
+			var anReturn = [];
+			var iCorrector;
+			var iRow, iRows, iColumn, iColumns;
+			
+			for ( iRow=0, iRows=nTrs.length ; iRow<iRows ; iRow++ )
+			{
+				nTds = [];
+				for ( iColumn=0, iColumns=nTrs[iRow].childNodes.length ; iColumn<iColumns ; iColumn++ )
+				{
+					nTd = nTrs[iRow].childNodes[iColumn];
+					if ( nTd.nodeName.toUpperCase() == "TD" )
+					{
+						nTds.push( nTd );
+					}
+				}
+				
+				iCorrector = 0;
+				for ( iColumn=0, iColumns=oSettings.aoColumns.length ; iColumn<iColumns ; iColumn++ )
+				{
+					if ( oSettings.aoColumns[iColumn].bVisible )
+					{
+						anReturn.push( nTds[iColumn-iCorrector] );
+					}
+					else
+					{
+						anReturn.push( oSettings.aoData[iRow]._anHidden[iColumn] );
+						iCorrector++;
+					}
+				}
+			}
+			return anReturn;
+		}
+		
+		/*
+		 * Function: _fnEscapeRegex
+		 * Purpose:  scape a string stuch that it can be used in a regular expression
+		 * Returns:  string: - escaped string
+		 * Inputs:   string:sVal - string to escape
+		 */
+		function _fnEscapeRegex ( sVal )
+		{
+			var acEscape = [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^' ];
+		  var reReplace = new RegExp( '(\\' + acEscape.join('|\\') + ')', 'g' );
+		  return sVal.replace(reReplace, '\\$1');
+		}
+		
+		/*
+		 * Function: _fnDeleteIndex
+		 * Purpose:  Take an array of integers (index array) and remove a target integer (value - not 
+		 *             the key!)
+		 * Returns:  -
+		 * Inputs:   a:array int - Index array to target
+		 *           int:iTarget - value to find
+		 */
+		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 );
+			}
+		}
+		
+		/*
+		 * Function: _fnReOrderIndex
+		 * Purpose:  Figure out how to reorder a display list
+		 * Returns:  array int:aiReturn - index list for reordering
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		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;
+		}
+		
+		/*
+		 * Function: _fnColumnOrdering
+		 * Purpose:  Get the column ordering that DataTables expects
+		 * Returns:  string: - comma separated list of names
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		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);
+		}
+		
+		/*
+		 * Function: _fnLog
+		 * Purpose:  Log an error message
+		 * Returns:  -
+		 * Inputs:   int:iLevel - log error messages, or display them to the user
+		 *           string:sMesg - error message
+		 */
+		function _fnLog( oSettings, iLevel, sMesg )
+		{
+			var sAlert = oSettings.sTableId === "" ?
+			 	"DataTables warning: " +sMesg :
+			 	"DataTables warning (table id = '"+oSettings.sTableId+"'): " +sMesg;
+			
+			if ( iLevel === 0 )
+			{
+				if ( _oExt.sErrMode == 'alert' )
+				{
+					alert( sAlert );
+				}
+				else
+				{
+					throw sAlert;
+				}
+				return;
+			}
+			else if ( typeof console != 'undefined' && typeof console.log != 'undefined' )
+			{
+				console.log( sAlert );
+			}
+		}
+		
+		/*
+		 * Function: _fnClearTable
+		 * Purpose:  Nuke the table
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		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 );
+		}
+		
+		/*
+		 * Function: _fnSaveState
+		 * Purpose:  Save the state of a table in a cookie such that the page can be reloaded
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 */
+		function _fnSaveState ( oSettings )
+		{
+			if ( !oSettings.oFeatures.bStateSave || typeof oSettings.bDestroying != 'undefined' )
+			{
+				return;
+			}
+			
+			/* Store the interesting variables */
+			var i, iLen, sTmp;
+			var sValue = "{";
+			sValue += '"iCreate":'+ new Date().getTime()+',';
+			sValue += '"iStart":'+ oSettings._iDisplayStart+',';
+			sValue += '"iEnd":'+ oSettings._iDisplayEnd+',';
+			sValue += '"iLength":'+ oSettings._iDisplayLength+',';
+			sValue += '"sFilter":"'+ encodeURIComponent(oSettings.oPreviousSearch.sSearch)+'",';
+			sValue += '"sFilterEsc":'+ !oSettings.oPreviousSearch.bRegex+',';
+			
+			sValue += '"aaSorting":[ ';
+			for ( i=0 ; i<oSettings.aaSorting.length ; i++ )
+			{
+				sValue += '['+oSettings.aaSorting[i][0]+',"'+oSettings.aaSorting[i][1]+'"],';
+			}
+			sValue = sValue.substring(0, sValue.length-1);
+			sValue += "],";
+			
+			sValue += '"aaSearchCols":[ ';
+			for ( i=0 ; i<oSettings.aoPreSearchCols.length ; i++ )
+			{
+				sValue += '["'+encodeURIComponent(oSettings.aoPreSearchCols[i].sSearch)+
+					'",'+!oSettings.aoPreSearchCols[i].bRegex+'],';
+			}
+			sValue = sValue.substring(0, sValue.length-1);
+			sValue += "],";
+			
+			sValue += '"abVisCols":[ ';
+			for ( i=0 ; i<oSettings.aoColumns.length ; i++ )
+			{
+				sValue += oSettings.aoColumns[i].bVisible+",";
+			}
+			sValue = sValue.substring(0, sValue.length-1);
+			sValue += "]";
+			
+			/* Save state from any plug-ins */
+			for ( i=0, iLen=oSettings.aoStateSave.length ; i<iLen ; i++ )
+			{
+				sTmp = oSettings.aoStateSave[i].fn( oSettings, sValue );
+				if ( sTmp !== "" )
+				{
+					sValue = sTmp;
+				}
+			}
+			
+			sValue += "}";
+			
+			_fnCreateCookie( oSettings.sCookiePrefix+oSettings.sInstance, sValue, 
+				oSettings.iCookieDuration, oSettings.sCookiePrefix, oSettings.fnCookieCallback );
+		}
+		
+		/*
+		 * Function: _fnLoadState
+		 * Purpose:  Attempt to load a saved table state from a cookie
+		 * Returns:  -
+		 * Inputs:   object:oSettings - dataTables settings object
+		 *           object:oInit - DataTables init object so we can override settings
+		 */
+		function _fnLoadState ( oSettings, oInit )
+		{
+			if ( !oSettings.oFeatures.bStateSave )
+			{
+				return;
+			}
+			
+			var oData, i, iLen;
+			var sData = _fnReadCookie( oSettings.sCookiePrefix+oSettings.sInstance );
+			if ( sData !== null && sData !== '' )
+			{
+				/* Try/catch the JSON eval - if it is bad then we ignore it - note that 1.7.0 and before
+				 * incorrectly used single quotes for some strings - hence the replace below
+				 */
+				try
+				{
+					oData = (typeof $.parseJSON == 'function') ? 
+						$.parseJSON( sData.replace(/'/g, '"') ) : eval( '('+sData+')' );
+				}
+				catch( e )
+				{
+					return;
+				}
+				
+				/* Allow custom and plug-in manipulation functions to alter the data set which was
+				 * saved, and also reject any saved state by returning false
+				 */
+				for ( i=0, iLen=oSettings.aoStateLoad.length ; i<iLen ; i++ )
+				{
+					if ( !oSettings.aoStateLoad[i].fn( oSettings, oData ) )
+					{
+						return;
+					}
+				}
+				
+				/* Store the saved state so it might be accessed at any time (particualrly a plug-in */
+				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.oPreviousSearch.sSearch = decodeURIComponent(oData.sFilter);
+				oSettings.aaSorting = oData.aaSorting.slice();
+				oSettings.saved_aaSorting = oData.aaSorting.slice();
+				
+				/*
+				 * Search filtering - global reference added in 1.4.1
+				 * Note that we use a 'not' for the value of the regular expression indicator to maintain
+				 * compatibility with pre 1.7 versions, where this was basically inverted. Added in 1.7.0
+				 */
+				if ( typeof oData.sFilterEsc != 'undefined' )
+				{
+					oSettings.oPreviousSearch.bRegex = !oData.sFilterEsc;
+				}
+				
+				/* Column filtering - added in 1.5.0 beta 6 */
+				if ( typeof oData.aaSearchCols != 'undefined' )
+				{
+					for ( i=0 ; i<oData.aaSearchCols.length ; i++ )
+					{
+						oSettings.aoPreSearchCols[i] = {
+							"sSearch": decodeURIComponent(oData.aaSearchCols[i][0]),
+							"bRegex": !oData.aaSearchCols[i][1]
+						};
+					}
+				}
+				
+				/* Column visibility state - added in 1.5.0 beta 10 */
+				if ( typeof oData.abVisCols != 'undefined' )
+				{
+					/* Pass back visibiliy 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 ( i=0 ; i<oData.abVisCols.length ; i++ )
+					{
+						oInit.saved_aoColumns[i] = {};
+						oInit.saved_aoColumns[i].bVisible = oData.abVisCols[i];
+					}
+				}
+			}
+		}
+		
+		/*
+		 * Function: _fnCreateCookie
+		 * Purpose:  Create a new cookie with a value to store the state of a table
+		 * Returns:  -
+		 * Inputs:   string:sName - name of the cookie to create
+		 *           string:sValue - the value the cookie should take
+		 *           int:iSecs - duration of the cookie
+		 *           string:sBaseName - sName is made up of the base + file name - this is the base
+		 *           function:fnCallback - User definable function to modify the cookie
+		 */
+		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. This is FAR from bullet proof
+			 */
+			var sOldName="", iOldTime=9999999999999;
+			var iLength = _fnReadCookie( sNameFile )!==null ? document.cookie.length : 
+				sFullCookie.length + document.cookie.length;
+			
+			if ( iLength+10 > 4096 ) /* Magic 10 for padding */
+			{
+				var aCookies =document.cookie.split(';');
+				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])+')' ); }
+						catch( e ) { continue; }
+						
+						if ( typeof oData.iCreate != 'undefined' && oData.iCreate < iOldTime )
+						{
+							sOldName = aSplitCookie[0];
+							iOldTime = oData.iCreate;
+						}
+					}
+				}
+				
+				if ( sOldName !== "" )
+				{
+					document.cookie = sOldName+"=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path="+
+						aParts.join('/') + "/";
+				}
+			}
+			
+			document.cookie = sFullCookie;
+		}
+		
+		/*
+		 * Function: _fnReadCookie
+		 * Purpose:  Read an old cookie to get a cookie with an old table state
+		 * Returns:  string: - contents of the cookie - or null if no cookie with that name found
+		 * Inputs:   string:sName - name of the cookie to read
+		 */
+		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;
+		}
+		
+		/*
+		 * Function: _fnGetUniqueThs
+		 * Purpose:  Get an array of unique th elements, one for each column
+		 * Returns:  array node:aReturn - list of unique ths
+		 * Inputs:   node:nThead - The thead element for the table
+		 */
+		function _fnGetUniqueThs ( nThead )
+		{
+			var nTrs = nThead.getElementsByTagName('tr');
+			
+			/* Nice simple case */
+			if ( nTrs.length == 1 )
+			{
+				return nTrs[0].getElementsByTagName('th');
+			}
+			
+			/* Otherwise we need to figure out the layout array to get the nodes */
+			var aLayout = [], aReturn = [];
+			var ROWSPAN = 2, COLSPAN = 3, TDELEM = 4;
+			var i, j, k, iLen, jLen, iColumnShifted;
+			var fnShiftCol = function ( a, i, j ) {
+				while ( typeof a[i][j] != 'undefined' ) {
+					j++;
+				}
+				return j;
+			};
+			var fnAddRow = function ( i ) {
+				if ( typeof aLayout[i] == 'undefined' ) {
+					aLayout[i] = [];
+				}
+			};
+			
+			/* Calculate a layout array */
+			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )
+			{
+				fnAddRow( i );
+				var iColumn = 0;
+				var nTds = [];
+				
+				for ( j=0, jLen=nTrs[i].childNodes.length ; j<jLen ; j++ )
+				{
+					if ( nTrs[i].childNodes[j].nodeName.toUpperCase() == "TD" ||
+					     nTrs[i].childNodes[j].nodeName.toUpperCase() == "TH" )
+					{
+						nTds.push( nTrs[i].childNodes[j] );
+					}
+				}
+				
+				for ( j=0, jLen=nTds.length ; j<jLen ; j++ )
+				{
+					var iColspan = nTds[j].getAttribute('colspan') * 1;
+					var iRowspan = nTds[j].getAttribute('rowspan') * 1;
+					
+					if ( !iColspan || iColspan===0 || iColspan===1 )
+					{
+						iColumnShifted = fnShiftCol( aLayout, i, iColumn );
+						aLayout[i][iColumnShifted] = (nTds[j].nodeName.toUpperCase()=="TD") ? TDELEM : nTds[j];
+						if ( iRowspan || iRowspan===0 || iRowspan===1 )
+						{
+							for ( k=1 ; k<iRowspan ; k++ )
+							{
+								fnAddRow( i+k );
+								aLayout[i+k][iColumnShifted] = ROWSPAN;
+							}
+						}
+						iColumn++;
+					}
+					else
+					{
+						iColumnShifted = fnShiftCol( aLayout, i, iColumn );
+						for ( k=0 ; k<iColspan ; k++ )
+						{
+							aLayout[i][iColumnShifted+k] = COLSPAN;
+						}
+						iColumn += iColspan;
+					}
+				}
+			}
+			
+			/* Convert the layout array into a node array */
+			for ( i=0, iLen=aLayout.length ; i<iLen ; i++ )
+			{
+				for ( j=0, jLen=aLayout[i].length ; j<jLen ; j++ )
+				{
+					if ( typeof aLayout[i][j] == 'object' && typeof aReturn[j] == 'undefined' )
+					{
+						aReturn[j] = aLayout[i][j];
+					}
+				}
+			}
+			
+			return aReturn;
+		}
+		
+		/*
+		 * Function: _fnScrollBarWidth
+		 * Purpose:  Get the width of a scroll bar in this browser being used
+		 * Returns:  int: - width in pixels
+		 * Inputs:   -
+		 * Notes:    All credit for this function belongs to Alexandre Gomes. Thanks for sharing!
+		 *   http://www.alexandre-gomes.com/?p=115
+		 */
+		function _fnScrollBarWidth ()
+		{  
+			var inner = document.createElement('p');  
+			var style = inner.style;
+			style.width = "100%";  
+			style.height = "200px";  
+			
+			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.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);  
+		}
+		
+		/*
+		 * Function: _fnApplyToChildren
+		 * Purpose:  Apply a given function to the display child nodes of an element array (typically
+		 *   TD children of TR rows
+		 * Returns:  - (done by reference)
+		 * Inputs:   function:fn - Method to apply to the objects
+		 *           array nodes:an1 - List of elements to look through for display children
+		 *           array nodes:an2 - Another list (identical structure to the first) - optional
+		 */
+		function _fnApplyToChildren( fn, an1, an2 )
+		{
+			for ( var i=0, iLen=an1.length ; i<iLen ; i++ )
+			{
+				for ( var j=0, jLen=an1[i].childNodes.length ; j<jLen ; j++ )
+				{
+					if ( an1[i].childNodes[j].nodeType == 1 )
+					{
+						if ( typeof an2 != 'undefined' )
+						{
+							fn( an1[i].childNodes[j], an2[i].childNodes[j] );
+						}
+						else
+						{
+							fn( an1[i].childNodes[j] );
+						}
+					}
+				}
+			}
+		}
+		
+		/*
+		 * Function: _fnMap
+		 * Purpose:  See if a property is defined on one object, if so assign it to the other object
+		 * Returns:  - (done by reference)
+		 * Inputs:   object:oRet - target object
+		 *           object:oSrc - source object
+		 *           string:sName - property
+		 *           string:sMappedName - name to map too - optional, sName used if not given
+		 */
+		function _fnMap( oRet, oSrc, sName, sMappedName )
+		{
+			if ( typeof sMappedName == 'undefined' )
+			{
+				sMappedName = sName;
+			}
+			if ( typeof oSrc[sName] != 'undefined' )
+			{
+				oRet[sMappedName] = oSrc[sName];
+			}
+		}
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - API
+		 * 
+		 * I'm not overly happy with this solution - I'd much rather that there was a way of getting
+		 * a list of all the private functions and do what we need to dynamically - but that doesn't
+		 * appear to be possible. Bonkers. A better solution would be to provide a 'bind' type object
+		 * To do - bind type method in DTs 2.x.
+		 */
+		this.oApi._fnExternApiFunc = _fnExternApiFunc;
+		this.oApi._fnInitalise = _fnInitalise;
+		this.oApi._fnLanguageProcess = _fnLanguageProcess;
+		this.oApi._fnAddColumn = _fnAddColumn;
+		this.oApi._fnColumnOptions = _fnColumnOptions;
+		this.oApi._fnAddData = _fnAddData;
+		this.oApi._fnGatherData = _fnGatherData;
+		this.oApi._fnDrawHead = _fnDrawHead;
+		this.oApi._fnDraw = _fnDraw;
+		this.oApi._fnReDraw = _fnReDraw;
+		this.oApi._fnAjaxUpdate = _fnAjaxUpdate;
+		this.oApi._fnAjaxUpdateDraw = _fnAjaxUpdateDraw;
+		this.oApi._fnAddOptionsHtml = _fnAddOptionsHtml;
+		this.oApi._fnFeatureHtmlTable = _fnFeatureHtmlTable;
+		this.oApi._fnScrollDraw = _fnScrollDraw;
+		this.oApi._fnAjustColumnSizing = _fnAjustColumnSizing;
+		this.oApi._fnFeatureHtmlFilter = _fnFeatureHtmlFilter;
+		this.oApi._fnFilterComplete = _fnFilterComplete;
+		this.oApi._fnFilterCustom = _fnFilterCustom;
+		this.oApi._fnFilterColumn = _fnFilterColumn;
+		this.oApi._fnFilter = _fnFilter;
+		this.oApi._fnBuildSearchArray = _fnBuildSearchArray;
+		this.oApi._fnBuildSearchRow = _fnBuildSearchRow;
+		this.oApi._fnFilterCreateSearch = _fnFilterCreateSearch;
+		this.oApi._fnDataToSearch = _fnDataToSearch;
+		this.oApi._fnSort = _fnSort;
+		this.oApi._fnSortAttachListener = _fnSortAttachListener;
+		this.oApi._fnSortingClasses = _fnSortingClasses;
+		this.oApi._fnFeatureHtmlPaginate = _fnFeatureHtmlPaginate;
+		this.oApi._fnPageChange = _fnPageChange;
+		this.oApi._fnFeatureHtmlInfo = _fnFeatureHtmlInfo;
+		this.oApi._fnUpdateInfo = _fnUpdateInfo;
+		this.oApi._fnFeatureHtmlLength = _fnFeatureHtmlLength;
+		this.oApi._fnFeatureHtmlProcessing = _fnFeatureHtmlProcessing;
+		this.oApi._fnProcessingDisplay = _fnProcessingDisplay;
+		this.oApi._fnVisibleToColumnIndex = _fnVisibleToColumnIndex;
+		this.oApi._fnColumnIndexToVisible = _fnColumnIndexToVisible;
+		this.oApi._fnNodeToDataIndex = _fnNodeToDataIndex;
+		this.oApi._fnVisbleColumns = _fnVisbleColumns;
+		this.oApi._fnCalculateEnd = _fnCalculateEnd;
+		this.oApi._fnConvertToWidth = _fnConvertToWidth;
+		this.oApi._fnCalculateColumnWidths = _fnCalculateColumnWidths;
+		this.oApi._fnScrollingWidthAdjust = _fnScrollingWidthAdjust;
+		this.oApi._fnGetWidestNode = _fnGetWidestNode;
+		this.oApi._fnGetMaxLenString = _fnGetMaxLenString;
+		this.oApi._fnStringToCss = _fnStringToCss;
+		this.oApi._fnArrayCmp = _fnArrayCmp;
+		this.oApi._fnDetectType = _fnDetectType;
+		this.oApi._fnSettingsFromNode = _fnSettingsFromNode;
+		this.oApi._fnGetDataMaster = _fnGetDataMaster;
+		this.oApi._fnGetTrNodes = _fnGetTrNodes;
+		this.oApi._fnGetTdNodes = _fnGetTdNodes;
+		this.oApi._fnEscapeRegex = _fnEscapeRegex;
+		this.oApi._fnDeleteIndex = _fnDeleteIndex;
+		this.oApi._fnReOrderIndex = _fnReOrderIndex;
+		this.oApi._fnColumnOrdering = _fnColumnOrdering;
+		this.oApi._fnLog = _fnLog;
+		this.oApi._fnClearTable = _fnClearTable;
+		this.oApi._fnSaveState = _fnSaveState;
+		this.oApi._fnLoadState = _fnLoadState;
+		this.oApi._fnCreateCookie = _fnCreateCookie;
+		this.oApi._fnReadCookie = _fnReadCookie;
+		this.oApi._fnGetUniqueThs = _fnGetUniqueThs;
+		this.oApi._fnScrollBarWidth = _fnScrollBarWidth;
+		this.oApi._fnApplyToChildren = _fnApplyToChildren;
+		this.oApi._fnMap = _fnMap;
+		
+		
+		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+		 * Section - Constructor
+		 */
+		
+		/* Want to be able to reference "this" inside the this.each function */
+		var _that = this;
+		return this.each(function()
+		{
+			var i=0, iLen, j, jLen, k, kLen;
+			
+			/* Check to see if we are re-initalising a table */
+			for ( i=0, iLen=_aoSettings.length ; i<iLen ; i++ )
+			{
+				/* Base check on table node */
+				if ( _aoSettings[i].nTable == this )
+				{
+					if ( typeof oInit == 'undefined' || 
+					   ( typeof oInit.bRetrieve != 'undefined' && oInit.bRetrieve === true ) )
+					{
+						return _aoSettings[i].oInstance;
+					}
+					else if ( typeof oInit.bDestroy != 'undefined' && oInit.bDestroy === true )
+					{
+						_aoSettings[i].oInstance.fnDestroy();
+						break;
+					}
+					else
+					{
+						_fnLog( _aoSettings[i], 0, "Cannot reinitialise DataTable.\n\n"+
+							"To retrieve the DataTables object for this table, please pass either no arguments "+
+							"to the dataTable() function, or set bRetrieve to true. Alternatively, to destory "+
+							"the old table and create a new one, set bDestroy to true (note that a lot of "+
+							"changes to the configuration can be made through the API which is usually much "+
+							"faster)." );
+						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 destory 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 ( _aoSettings[i].sTableId !== "" && _aoSettings[i].sTableId == this.getAttribute('id') )
+				{
+					_aoSettings.splice( i, 1 );
+					break;
+				}
+			}
+			
+			/* Make a complete and independent copy of the settings object */
+			var oSettings = new classSettings();
+			_aoSettings.push( oSettings );
+			
+			var bInitHandedOff = false;
+			var bUsePassedData = false;
+			
+			/* Set the id */
+			var sId = this.getAttribute( 'id' );
+			if ( sId !== null )
+			{
+				oSettings.sTableId = sId;
+				oSettings.sInstance = sId;
+			}
+			else
+			{
+				oSettings.sInstance = _oExt._oExternConfig.iNextUnique ++;
+			}
+			
+			/* Sanity check */
+			if ( this.nodeName.toLowerCase() != 'table' )
+			{
+				_fnLog( oSettings, 0, "Attempted to initialise DataTables on a node which is not a "+
+					"table: "+this.nodeName );
+				return;
+			}
+			
+			/* Set the table node */
+			oSettings.nTable = this;
+			
+			/* Keep a reference to the 'this' instance for the table. Note that if this table is being
+			 * created with others, we retrieve a unique instance to ease API access.
+			 */
+			oSettings.oInstance = _that.length == 1 ? _that : $(this).dataTable();
+			
+			/* Bind the API functions to the settings, so we can perform actions whenever oSettings is
+			 * available
+			 */
+			oSettings.oApi = _that.oApi;
+			
+			/* State the table's width for if a destroy is called at a later time */
+			oSettings.sDestroyWidth = $(this).width();
+			
+			/* Store the features that we have available */
+			if ( typeof oInit != 'undefined' && oInit !== null )
+			{
+				oSettings.oInit = oInit;
+				_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.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, "asStripClasses" );
+				_fnMap( oSettings, oInit, "fnRowCallback" );
+				_fnMap( oSettings, oInit, "fnHeaderCallback" );
+				_fnMap( oSettings, oInit, "fnFooterCallback" );
+				_fnMap( oSettings, oInit, "fnCookieCallback" );
+				_fnMap( oSettings, oInit, "fnInitComplete" );
+				_fnMap( oSettings, oInit, "fnServerData" );
+				_fnMap( oSettings, oInit, "fnFormatNumber" );
+				_fnMap( oSettings, oInit, "aaSorting" );
+				_fnMap( oSettings, oInit, "aaSortingFixed" );
+				_fnMap( oSettings, oInit, "aLengthMenu" );
+				_fnMap( oSettings, oInit, "sPaginationType" );
+				_fnMap( oSettings, oInit, "sAjaxSource" );
+				_fnMap( oSettings, oInit, "iCookieDuration" );
+				_fnMap( oSettings, oInit, "sCookiePrefix" );
+				_fnMap( oSettings, oInit, "sDom" );
+				_fnMap( oSettings, oInit, "oSearch", "oPreviousSearch" );
+				_fnMap( oSettings, oInit, "aoSearchCols", "aoPreSearchCols" );
+				_fnMap( oSettings, oInit, "iDisplayLength", "_iDisplayLength" );
+				_fnMap( oSettings, oInit, "bJQueryUI", "bJUI" );
+				_fnMap( oSettings.oLanguage, oInit, "fnInfoCallback" );
+				
+				/* Callback functions which are array driven */
+				if ( typeof oInit.fnDrawCallback == 'function' )
+				{
+					oSettings.aoDrawCallback.push( {
+						"fn": oInit.fnDrawCallback,
+						"sName": "user"
+					} );
+				}
+				
+				if ( typeof oInit.fnStateSaveCallback == 'function' )
+				{
+					oSettings.aoStateSave.push( {
+						"fn": oInit.fnStateSaveCallback,
+						"sName": "user"
+					} );
+				}
+				
+				if ( typeof oInit.fnStateLoadCallback == 'function' )
+				{
+					oSettings.aoStateLoad.push( {
+						"fn": oInit.fnStateLoadCallback,
+						"sName": "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
+					 */
+					oSettings.aoDrawCallback.push( {
+						"fn": _fnSortingClasses,
+						"sName": "server_side_sort_classes"
+					} );
+				}
+				
+				if ( typeof oInit.bJQueryUI != 'undefined' && 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 
+					 */
+					oSettings.oClasses = _oExt.oJUIClasses;
+					
+					if ( typeof oInit.sDom == 'undefined' )
+					{
+						/* Set the DOM to use a layout suitable for jQuery UI's theming */
+						oSettings.sDom = '<"H"lfr>t<"F"ip>';
+					}
+				}
+				
+				/* Calculate the scroll bar width and cache it for use later on */
+				if ( oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "" )
+				{
+					oSettings.oScroll.iBarWidth = _fnScrollBarWidth();
+				}
+				
+				if ( typeof oInit.iDisplayStart != 'undefined' && 
+				     typeof 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 ( typeof oInit.bStateSave != 'undefined' )
+				{
+					oSettings.oFeatures.bStateSave = oInit.bStateSave;
+					_fnLoadState( oSettings, oInit );
+					oSettings.aoDrawCallback.push( {
+						"fn": _fnSaveState,
+						"sName": "state_save"
+					} );
+				}
+				
+				if ( typeof oInit.aaData != 'undefined' )
+				{
+					bUsePassedData = true;
+				}
+				
+				/* Backwards compatability */
+				/* aoColumns / aoData - remove at some point... */
+				if ( typeof oInit != 'undefined' && typeof oInit.aoData != 'undefined' )
+				{
+					oInit.aoColumns = oInit.aoData;
+				}
+				
+				/* Language definitions */
+				if ( typeof oInit.oLanguage != 'undefined' )
+				{
+					if ( typeof oInit.oLanguage.sUrl != 'undefined' && oInit.oLanguage.sUrl !== "" )
+					{
+						/* Get the language definitions from a file */
+						oSettings.oLanguage.sUrl = oInit.oLanguage.sUrl;
+						$.getJSON( oSettings.oLanguage.sUrl, null, function( json ) { 
+							_fnLanguageProcess( oSettings, json, true ); } );
+						bInitHandedOff = true;
+					}
+					else
+					{
+						_fnLanguageProcess( oSettings, oInit.oLanguage, false );
+					}
+				}
+				/* Warning: The _fnLanguageProcess function is async to the remainder of this function due
+				 * to the XHR. We use _bInitialised in _fnLanguageProcess() to check this the processing 
+				 * below is complete. The reason for spliting it like this is optimisation - we can fire
+				 * off the XHR (if needed) and then continue processing the data.
+				 */
+			}
+			else
+			{
+				/* Create a dummy object for quick manipulation later on. */
+				oInit = {};
+			}
+			
+			/*
+			 * Stripes
+			 * Add the strip classes now that we know which classes to apply - unless overruled
+			 */
+			if ( typeof oInit.asStripClasses == 'undefined' )
+			{
+				oSettings.asStripClasses.push( oSettings.oClasses.sStripOdd );
+				oSettings.asStripClasses.push( oSettings.oClasses.sStripEven );
+			}
+			
+			/* Remove row stripe classes if they are already on the table row */
+			var bStripeRemove = false;
+			var anRows = $('>tbody>tr', this);
+			for ( i=0, iLen=oSettings.asStripClasses.length ; i<iLen ; i++ )
+			{
+				if ( anRows.filter(":lt(2)").hasClass( oSettings.asStripClasses[i]) )
+				{
+					bStripeRemove = true;
+					break;
+				}
+			}
+					
+			if ( bStripeRemove )
+			{
+				/* Store the classes which we are about to remove so they can be readded on destory */
+				oSettings.asDestoryStrips = [ '', '' ];
+				if ( $(anRows[0]).hasClass(oSettings.oClasses.sStripOdd) )
+				{
+					oSettings.asDestoryStrips[0] += oSettings.oClasses.sStripOdd+" ";
+				}
+				if ( $(anRows[0]).hasClass(oSettings.oClasses.sStripEven) )
+				{
+					oSettings.asDestoryStrips[0] += oSettings.oClasses.sStripEven;
+				}
+				if ( $(anRows[1]).hasClass(oSettings.oClasses.sStripOdd) )
+				{
+					oSettings.asDestoryStrips[1] += oSettings.oClasses.sStripOdd+" ";
+				}
+				if ( $(anRows[1]).hasClass(oSettings.oClasses.sStripEven) )
+				{
+					oSettings.asDestoryStrips[1] += oSettings.oClasses.sStripEven;
+				}
+				
+				anRows.removeClass( oSettings.asStripClasses.join(' ') );
+			}
+			
+			/*
+			 * Columns
+			 * See if we should load columns automatically or use defined ones
+			 */
+			var nThead = this.getElementsByTagName('thead');
+			var anThs = nThead.length===0 ? [] : _fnGetUniqueThs( nThead[0] );
+			var aoColumnsInit;
+			
+			/* If not given a column array, generate one with nulls */
+			if ( typeof oInit.aoColumns == 'undefined' )
+			{
+				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++ )
+			{
+				/* Check if we have column visibilty state to restore */
+				if ( typeof 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 );
+			}
+			
+			/* Add options from column definations */
+			if ( typeof oInit.aoColumnDefs != 'undefined' )
+			{
+				/* Loop over the column defs array - loop in reverse so first instace has priority */
+				for ( i=oInit.aoColumnDefs.length-1 ; i>=0 ; i-- )
+				{
+					/* Each column def can target multiple columns, as it is an array */
+					var aTargets = oInit.aoColumnDefs[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 )
+						{
+							/* 0+ integer, left to right column counting. We add columns which are unknown
+							 * automatically. Is this the right behaviour for this? We should at least
+							 * log it in future. We cannot do this for the negative or class targets, only here.
+							 */
+							while( oSettings.aoColumns.length <= aTargets[j] )
+							{
+								_fnAddColumn( oSettings );
+							}
+							_fnColumnOptions( oSettings, aTargets[j], oInit.aoColumnDefs[i] );
+						}
+						else if ( typeof aTargets[j] == 'number' && aTargets[j] < 0 )
+						{
+							/* Negative integer, right to left column counting */
+							_fnColumnOptions( oSettings, oSettings.aoColumns.length+aTargets[j], 
+								oInit.aoColumnDefs[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.className.indexOf( aTargets[j] ) != -1 )
+								{
+									_fnColumnOptions( oSettings, k, oInit.aoColumnDefs[i] );
+								}
+							}
+						}
+					}
+				}
+			}
+			
+			/* Add options from column array - after the defs array so this has priority */
+			if ( typeof aoColumnsInit != 'undefined' )
+			{
+				for ( i=0, iLen=aoColumnsInit.length ; i<iLen ; i++ )
+				{
+					_fnColumnOptions( oSettings, i, aoColumnsInit[i] );
+				}
+			}
+			
+			/*
+			 * 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 ( typeof oSettings.aaSorting[i][2] == 'undefined' )
+				{
+					oSettings.aaSorting[i][2] = 0;
+				}
+				
+				/* If aaSorting is not defined, then we use the first indicator in asSorting */
+				if ( typeof oInit.aaSorting == "undefined" && 
+						 typeof 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
+			 * Sanity check that there is a thead and tbody. If not let's just create them
+			 */
+			if ( this.getElementsByTagName('thead').length === 0 )
+			{
+				this.appendChild( document.createElement( 'thead' ) );
+			}
+			
+			if ( this.getElementsByTagName('tbody').length === 0 )
+			{
+				this.appendChild( document.createElement( 'tbody' ) );
+			}
+			
+			oSettings.nTHead = this.getElementsByTagName('thead')[0];
+			oSettings.nTBody = this.getElementsByTagName('tbody')[0];
+			if ( this.getElementsByTagName('tfoot').length > 0 )
+			{
+				oSettings.nTFoot = this.getElementsByTagName('tfoot')[0];
+			}
+			
+			/* 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 )
+			{
+				_fnInitalise( oSettings );
+			}
+		});
+	};
+})(jQuery, window, document);
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/jquery.dataTables.min.js
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/jquery.dataTables.min.js	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/jquery.dataTables.min.js	(revision 10886)
@@ -0,0 +1,148 @@
+/*
+ * File:        jquery.dataTables.min.js
+ * Version:     1.7.6
+ * Author:      Allan Jardine (www.sprymedia.co.uk)
+ * Info:        www.datatables.net
+ * 
+ * Copyright 2008-2010 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.
+ *
+ * Customized by Henrik Bettermann: this.aLengthMenu, this._iDisplayLength, this._iDisplayEnd
+ *
+ */
+(function(j,ra,p){j.fn.dataTableSettings=[];var D=j.fn.dataTableSettings;j.fn.dataTableExt={};var n=j.fn.dataTableExt;n.sVersion="1.7.6";n.sErrMode="alert";n.iApiIndex=0;n.oApi={};n.afnFiltering=[];n.aoFeatures=[];n.ofnSearch={};n.afnSortData=[];n.oStdClasses={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",sPageFirst:"first",sPagePrevious:"previous",sPageNext:"next",sPageLast:"last",sStripOdd:"odd",sStripEven:"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:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sFooterTH:""};n.oJUIClasses={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",
+sPagePrevious:"previous",sPageNext:"next",sPageLast:"last ui-corner-tr ui-corner-br",sStripOdd:"odd",sStripEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi ui-buttonset-multi paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"ui-state-default",sSortDesc:"ui-state-default",sSortable:"ui-state-default",sSortableAsc:"ui-state-default",
+sSortableDesc:"ui-state-default",sSortableNone:"ui-state-default",sSortColumn:"sorting_",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",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead ui-state-default",sScrollHeadInner:"dataTables_scrollHeadInner",
+sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot ui-state-default",sScrollFootInner:"dataTables_scrollFootInner",sFooterTH:"ui-state-default"};n.oPagination={two_button:{fnInit:function(g,m,r){var s,w,y;if(g.bJUI){s=p.createElement("a");w=p.createElement("a");y=p.createElement("span");y.className=g.oClasses.sPageJUINext;w.appendChild(y);y=p.createElement("span");y.className=g.oClasses.sPageJUIPrev;s.appendChild(y)}else{s=p.createElement("div");w=p.createElement("div")}s.className=
+g.oClasses.sPagePrevDisabled;w.className=g.oClasses.sPageNextDisabled;s.title=g.oLanguage.oPaginate.sPrevious;w.title=g.oLanguage.oPaginate.sNext;m.appendChild(s);m.appendChild(w);j(s).bind("click.DT",function(){g.oApi._fnPageChange(g,"previous")&&r(g)});j(w).bind("click.DT",function(){g.oApi._fnPageChange(g,"next")&&r(g)});j(s).bind("selectstart.DT",function(){return false});j(w).bind("selectstart.DT",function(){return false});if(g.sTableId!==""&&typeof g.aanFeatures.p=="undefined"){m.setAttribute("id",
+g.sTableId+"_paginate");s.setAttribute("id",g.sTableId+"_previous");w.setAttribute("id",g.sTableId+"_next")}},fnUpdate:function(g){if(g.aanFeatures.p)for(var m=g.aanFeatures.p,r=0,s=m.length;r<s;r++)if(m[r].childNodes.length!==0){m[r].childNodes[0].className=g._iDisplayStart===0?g.oClasses.sPagePrevDisabled:g.oClasses.sPagePrevEnabled;m[r].childNodes[1].className=g.fnDisplayEnd()==g.fnRecordsDisplay()?g.oClasses.sPageNextDisabled:g.oClasses.sPageNextEnabled}}},iFullNumbersShowPages:5,full_numbers:{fnInit:function(g,
+m,r){var s=p.createElement("span"),w=p.createElement("span"),y=p.createElement("span"),F=p.createElement("span"),x=p.createElement("span");s.innerHTML=g.oLanguage.oPaginate.sFirst;w.innerHTML=g.oLanguage.oPaginate.sPrevious;F.innerHTML=g.oLanguage.oPaginate.sNext;x.innerHTML=g.oLanguage.oPaginate.sLast;var u=g.oClasses;s.className=u.sPageButton+" "+u.sPageFirst;w.className=u.sPageButton+" "+u.sPagePrevious;F.className=u.sPageButton+" "+u.sPageNext;x.className=u.sPageButton+" "+u.sPageLast;m.appendChild(s);
+m.appendChild(w);m.appendChild(y);m.appendChild(F);m.appendChild(x);j(s).bind("click.DT",function(){g.oApi._fnPageChange(g,"first")&&r(g)});j(w).bind("click.DT",function(){g.oApi._fnPageChange(g,"previous")&&r(g)});j(F).bind("click.DT",function(){g.oApi._fnPageChange(g,"next")&&r(g)});j(x).bind("click.DT",function(){g.oApi._fnPageChange(g,"last")&&r(g)});j("span",m).bind("mousedown.DT",function(){return false}).bind("selectstart.DT",function(){return false});if(g.sTableId!==""&&typeof g.aanFeatures.p==
+"undefined"){m.setAttribute("id",g.sTableId+"_paginate");s.setAttribute("id",g.sTableId+"_first");w.setAttribute("id",g.sTableId+"_previous");F.setAttribute("id",g.sTableId+"_next");x.setAttribute("id",g.sTableId+"_last")}},fnUpdate:function(g,m){if(g.aanFeatures.p){var r=n.oPagination.iFullNumbersShowPages,s=Math.floor(r/2),w=Math.ceil(g.fnRecordsDisplay()/g._iDisplayLength),y=Math.ceil(g._iDisplayStart/g._iDisplayLength)+1,F="",x,u=g.oClasses;if(w<r){s=1;x=w}else if(y<=s){s=1;x=r}else if(y>=w-s){s=
+w-r+1;x=w}else{s=y-Math.ceil(r/2)+1;x=s+r-1}for(r=s;r<=x;r++)F+=y!=r?'<span class="'+u.sPageButton+'">'+r+"</span>":'<span class="'+u.sPageButtonActive+'">'+r+"</span>";x=g.aanFeatures.p;var z,U=function(){g._iDisplayStart=(this.innerHTML*1-1)*g._iDisplayLength;m(g);return false},C=function(){return false};r=0;for(s=x.length;r<s;r++)if(x[r].childNodes.length!==0){z=j("span:eq(2)",x[r]);z.html(F);j("span",z).bind("click.DT",U).bind("mousedown.DT",C).bind("selectstart.DT",C);z=x[r].getElementsByTagName("span");
+z=[z[0],z[1],z[z.length-2],z[z.length-1]];j(z).removeClass(u.sPageButton+" "+u.sPageButtonActive+" "+u.sPageButtonStaticDisabled);if(y==1){z[0].className+=" "+u.sPageButtonStaticDisabled;z[1].className+=" "+u.sPageButtonStaticDisabled}else{z[0].className+=" "+u.sPageButton;z[1].className+=" "+u.sPageButton}if(w===0||y==w||g._iDisplayLength==-1){z[2].className+=" "+u.sPageButtonStaticDisabled;z[3].className+=" "+u.sPageButtonStaticDisabled}else{z[2].className+=" "+u.sPageButton;z[3].className+=" "+
+u.sPageButton}}}}}};n.oSort={"string-asc":function(g,m){g=g.toLowerCase();m=m.toLowerCase();return g<m?-1:g>m?1:0},"string-desc":function(g,m){g=g.toLowerCase();m=m.toLowerCase();return g<m?1:g>m?-1:0},"html-asc":function(g,m){g=g.replace(/<.*?>/g,"").toLowerCase();m=m.replace(/<.*?>/g,"").toLowerCase();return g<m?-1:g>m?1:0},"html-desc":function(g,m){g=g.replace(/<.*?>/g,"").toLowerCase();m=m.replace(/<.*?>/g,"").toLowerCase();return g<m?1:g>m?-1:0},"date-asc":function(g,m){g=Date.parse(g);m=Date.parse(m);
+if(isNaN(g)||g==="")g=Date.parse("01/01/1970 00:00:00");if(isNaN(m)||m==="")m=Date.parse("01/01/1970 00:00:00");return g-m},"date-desc":function(g,m){g=Date.parse(g);m=Date.parse(m);if(isNaN(g)||g==="")g=Date.parse("01/01/1970 00:00:00");if(isNaN(m)||m==="")m=Date.parse("01/01/1970 00:00:00");return m-g},"numeric-asc":function(g,m){return(g=="-"||g===""?0:g*1)-(m=="-"||m===""?0:m*1)},"numeric-desc":function(g,m){return(m=="-"||m===""?0:m*1)-(g=="-"||g===""?0:g*1)}};n.aTypes=[function(g){if(g.length===
+0)return"numeric";var m,r=false;m=g.charAt(0);if("0123456789-".indexOf(m)==-1)return null;for(var s=1;s<g.length;s++){m=g.charAt(s);if("0123456789.".indexOf(m)==-1)return null;if(m=="."){if(r)return null;r=true}}return"numeric"},function(g){var m=Date.parse(g);if(m!==null&&!isNaN(m)||g.length===0)return"date";return null},function(g){if(g.indexOf("<")!=-1&&g.indexOf(">")!=-1)return"html";return null}];n.fnVersionCheck=function(g){var m=function(x,u){for(;x.length<u;)x+="0";return x},r=n.sVersion.split(".");
+g=g.split(".");for(var s="",w="",y=0,F=g.length;y<F;y++){s+=m(r[y],3);w+=m(g[y],3)}return parseInt(s,10)>=parseInt(w,10)};n._oExternConfig={iNextUnique:0};j.fn.dataTable=function(g){function m(){this.fnRecordsTotal=function(){return this.oFeatures.bServerSide?parseInt(this._iRecordsTotal,10):this.aiDisplayMaster.length};this.fnRecordsDisplay=function(){return this.oFeatures.bServerSide?parseInt(this._iRecordsDisplay,10):this.aiDisplay.length};this.fnDisplayEnd=function(){return this.oFeatures.bServerSide?
+this.oFeatures.bPaginate===false||this._iDisplayLength==-1?this._iDisplayStart+this.aiDisplay.length:Math.min(this._iDisplayStart+this._iDisplayLength,this._iRecordsDisplay):this._iDisplayEnd};this.sInstance=this.oInstance=null;this.oFeatures={bPaginate:true,bLengthChange:true,bFilter:true,bSort:true,bInfo:true,bAutoWidth:true,bProcessing:false,bSortClasses:true,bStateSave:false,bServerSide:false};this.oScroll={sX:"",sXInner:"",sY:"",bCollapse:false,bInfinite:false,iLoadGap:100,iBarWidth:0,bAutoCss:true};
+this.aanFeatures=[];this.oLanguage={sProcessing:"Processing...",sLengthMenu:"Show _MENU_ entries",sZeroRecords:"No matching records found",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:"",sSearch:"Search:",sUrl:"",oPaginate:{sFirst:"First",sPrevious:"Previous",sNext:"Next",sLast:"Last"},fnInfoCallback:null};this.aoData=[];this.aiDisplay=[];this.aiDisplayMaster=
+[];this.aoColumns=[];this.iNextId=0;this.asDataSearch=[];this.oPreviousSearch={sSearch:"",bRegex:false,bSmart:true};this.aoPreSearchCols=[];this.aaSorting=[[0,"asc",0]];this.aaSortingFixed=null;this.asStripClasses=[];this.asDestoryStrips=[];this.sDestroyWidth=0;this.fnFooterCallback=this.fnHeaderCallback=this.fnRowCallback=null;this.aoDrawCallback=[];this.fnInitComplete=null;this.sTableId="";this.nTableWrapper=this.nTBody=this.nTFoot=this.nTHead=this.nTable=null;this.bInitialised=false;this.aoOpenRows=
+[];this.sDom="lfrtip";this.sPaginationType="two_button";this.iCookieDuration=7200;this.sCookiePrefix="SpryMedia_DataTables_";this.fnCookieCallback=null;this.aoStateSave=[];this.aoStateLoad=[];this.sAjaxSource=this.oLoadedState=null;this.bAjaxDataGet=true;this.fnServerData=function(a,b,c){j.ajax({url:a,data:b,success:c,dataType:"json",cache:false,error:function(d,f){f=="parsererror"&&alert("DataTables warning: JSON data from server could not be parsed. This is caused by a JSON formatting error.")}})};
+this.fnFormatNumber=function(a){if(a<1E3)return a;else{var b=a+"";a=b.split("");var c="";b=b.length;for(var d=0;d<b;d++){if(d%3===0&&d!==0)c=","+c;c=a[b-d-1]+c}}return c};this.aLengthMenu=[10,25,50,100,1000,5000];this.bDrawing=this.iDraw=0;this.iDrawError=-1;this._iDisplayLength=1000;this._iDisplayStart=0;this._iDisplayEnd=1000;this._iRecordsDisplay=this._iRecordsTotal=0;this.bJUI=false;this.oClasses=n.oStdClasses;this.bSorted=this.bFiltered=false;this.oInit=null}function r(a){return function(){var b=[A(this[n.iApiIndex])].concat(Array.prototype.slice.call(arguments));
+return n.oApi[a].apply(this,b)}}function s(a){var b,c;if(a.bInitialised===false)setTimeout(function(){s(a)},200);else{sa(a);U(a);K(a,true);a.oFeatures.bAutoWidth&&$(a);b=0;for(c=a.aoColumns.length;b<c;b++)if(a.aoColumns[b].sWidth!==null)a.aoColumns[b].nTh.style.width=v(a.aoColumns[b].sWidth);if(a.oFeatures.bSort)O(a);else{a.aiDisplay=a.aiDisplayMaster.slice();E(a);C(a)}if(a.sAjaxSource!==null&&!a.oFeatures.bServerSide)a.fnServerData.call(a.oInstance,a.sAjaxSource,[],function(d){for(b=0;b<d.aaData.length;b++)u(a,
+d.aaData[b]);a.iInitDisplayStart=a._iDisplayStart;if(a.oFeatures.bSort)O(a);else{a.aiDisplay=a.aiDisplayMaster.slice();E(a);C(a)}K(a,false);w(a,d)});else if(!a.oFeatures.bServerSide){K(a,false);w(a)}}}function w(a,b){a._bInitComplete=true;if(typeof a.fnInitComplete=="function")typeof b!="undefined"?a.fnInitComplete.call(a.oInstance,a,b):a.fnInitComplete.call(a.oInstance,a)}function y(a,b,c){o(a.oLanguage,b,"sProcessing");o(a.oLanguage,b,"sLengthMenu");o(a.oLanguage,b,"sEmptyTable");o(a.oLanguage,
+b,"sZeroRecords");o(a.oLanguage,b,"sInfo");o(a.oLanguage,b,"sInfoEmpty");o(a.oLanguage,b,"sInfoFiltered");o(a.oLanguage,b,"sInfoPostFix");o(a.oLanguage,b,"sSearch");if(typeof b.oPaginate!="undefined"){o(a.oLanguage.oPaginate,b.oPaginate,"sFirst");o(a.oLanguage.oPaginate,b.oPaginate,"sPrevious");o(a.oLanguage.oPaginate,b.oPaginate,"sNext");o(a.oLanguage.oPaginate,b.oPaginate,"sLast")}typeof b.sEmptyTable=="undefined"&&typeof b.sZeroRecords!="undefined"&&o(a.oLanguage,b,"sZeroRecords","sEmptyTable");
+c&&s(a)}function F(a,b){a.aoColumns[a.aoColumns.length++]={sType:null,_bAutoType:true,bVisible:true,bSearchable:true,bSortable:true,asSorting:["asc","desc"],sSortingClass:a.oClasses.sSortable,sSortingClassJUI:a.oClasses.sSortJUI,sTitle:b?b.innerHTML:"",sName:"",sWidth:null,sWidthOrig:null,sClass:null,fnRender:null,bUseRendered:true,iDataSort:a.aoColumns.length-1,sSortDataType:"std",nTh:b?b:p.createElement("th"),nTf:null,anThExtra:[],anTfExtra:[]};b=a.aoColumns.length-1;if(typeof a.aoPreSearchCols[b]==
+"undefined"||a.aoPreSearchCols[b]===null)a.aoPreSearchCols[b]={sSearch:"",bRegex:false,bSmart:true};else{if(typeof a.aoPreSearchCols[b].bRegex=="undefined")a.aoPreSearchCols[b].bRegex=true;if(typeof a.aoPreSearchCols[b].bSmart=="undefined")a.aoPreSearchCols[b].bSmart=true}x(a,b,null)}function x(a,b,c){b=a.aoColumns[b];if(typeof c!="undefined"&&c!==null){if(typeof c.sType!="undefined"){b.sType=c.sType;b._bAutoType=false}o(b,c,"bVisible");o(b,c,"bSearchable");o(b,c,"bSortable");o(b,c,"sTitle");o(b,
+c,"sName");o(b,c,"sWidth");o(b,c,"sWidth","sWidthOrig");o(b,c,"sClass");o(b,c,"fnRender");o(b,c,"bUseRendered");o(b,c,"iDataSort");o(b,c,"asSorting");o(b,c,"sSortDataType")}if(!a.oFeatures.bSort)b.bSortable=false;if(!b.bSortable||j.inArray("asc",b.asSorting)==-1&&j.inArray("desc",b.asSorting)==-1){b.sSortingClass=a.oClasses.sSortableNone;b.sSortingClassJUI=""}else if(j.inArray("asc",b.asSorting)!=-1&&j.inArray("desc",b.asSorting)==-1){b.sSortingClass=a.oClasses.sSortableAsc;b.sSortingClassJUI=a.oClasses.sSortJUIAscAllowed}else if(j.inArray("asc",
+b.asSorting)==-1&&j.inArray("desc",b.asSorting)!=-1){b.sSortingClass=a.oClasses.sSortableDesc;b.sSortingClassJUI=a.oClasses.sSortJUIDescAllowed}}function u(a,b){if(b.length!=a.aoColumns.length&&a.iDrawError!=a.iDraw){H(a,0,"Added data (size "+b.length+") does not match known number of columns ("+a.aoColumns.length+")");a.iDrawError=a.iDraw;return-1}b=b.slice();var c=a.aoData.length;a.aoData.push({nTr:p.createElement("tr"),_iId:a.iNextId++,_aData:b,_anHidden:[],_sRowStripe:""});for(var d,f,e=0;e<b.length;e++){d=
+p.createElement("td");if(b[e]===null)b[e]="";if(typeof a.aoColumns[e].fnRender=="function"){f=a.aoColumns[e].fnRender({iDataRow:c,iDataColumn:e,aData:b,oSettings:a});d.innerHTML=f;if(a.aoColumns[e].bUseRendered)a.aoData[c]._aData[e]=f}else d.innerHTML=b[e];if(typeof b[e]!="string")b[e]+="";b[e]=j.trim(b[e]);if(a.aoColumns[e].sClass!==null)d.className=a.aoColumns[e].sClass;if(a.aoColumns[e]._bAutoType&&a.aoColumns[e].sType!="string"){f=aa(a.aoData[c]._aData[e]);if(a.aoColumns[e].sType===null)a.aoColumns[e].sType=
+f;else if(a.aoColumns[e].sType!=f)a.aoColumns[e].sType="string"}if(a.aoColumns[e].bVisible){a.aoData[c].nTr.appendChild(d);a.aoData[c]._anHidden[e]=null}else a.aoData[c]._anHidden[e]=d}a.aiDisplayMaster.push(c);return c}function z(a){var b,c,d,f,e,i,h,k;if(a.sAjaxSource===null){h=a.nTBody.childNodes;b=0;for(c=h.length;b<c;b++)if(h[b].nodeName.toUpperCase()=="TR"){i=a.aoData.length;a.aoData.push({nTr:h[b],_iId:a.iNextId++,_aData:[],_anHidden:[],_sRowStripe:""});a.aiDisplayMaster.push(i);k=a.aoData[i]._aData;
+i=h[b].childNodes;d=e=0;for(f=i.length;d<f;d++)if(i[d].nodeName.toUpperCase()=="TD"){k[e]=j.trim(i[d].innerHTML);e++}}}h=R(a);i=[];b=0;for(c=h.length;b<c;b++){d=0;for(f=h[b].childNodes.length;d<f;d++){e=h[b].childNodes[d];e.nodeName.toUpperCase()=="TD"&&i.push(e)}}i.length!=h.length*a.aoColumns.length&&H(a,1,"Unexpected number of TD elements. Expected "+h.length*a.aoColumns.length+" and got "+i.length+". DataTables does not support rowspan / colspan in the table body, and there must be one cell for each row/column combination.");
+h=0;for(d=a.aoColumns.length;h<d;h++){if(a.aoColumns[h].sTitle===null)a.aoColumns[h].sTitle=a.aoColumns[h].nTh.innerHTML;f=a.aoColumns[h]._bAutoType;e=typeof a.aoColumns[h].fnRender=="function";k=a.aoColumns[h].sClass!==null;var l=a.aoColumns[h].bVisible,q,t;if(f||e||k||!l){b=0;for(c=a.aoData.length;b<c;b++){q=i[b*d+h];if(f)if(a.aoColumns[h].sType!="string"){t=aa(a.aoData[b]._aData[h]);if(a.aoColumns[h].sType===null)a.aoColumns[h].sType=t;else if(a.aoColumns[h].sType!=t)a.aoColumns[h].sType="string"}if(e){t=
+a.aoColumns[h].fnRender({iDataRow:b,iDataColumn:h,aData:a.aoData[b]._aData,oSettings:a});q.innerHTML=t;if(a.aoColumns[h].bUseRendered)a.aoData[b]._aData[h]=t}if(k)q.className+=" "+a.aoColumns[h].sClass;if(l)a.aoData[b]._anHidden[h]=null;else{a.aoData[b]._anHidden[h]=q;q.parentNode.removeChild(q)}}}}}function U(a){var b,c,d,f,e,i=a.nTHead.getElementsByTagName("tr"),h=0,k;if(a.nTHead.getElementsByTagName("th").length!==0){b=0;for(d=a.aoColumns.length;b<d;b++){c=a.aoColumns[b].nTh;a.aoColumns[b].sClass!==
+null&&j(c).addClass(a.aoColumns[b].sClass);f=1;for(e=i.length;f<e;f++){k=j(i[f]).children();a.aoColumns[b].anThExtra.push(k[b-h]);a.aoColumns[b].bVisible||i[f].removeChild(k[b-h])}if(a.aoColumns[b].bVisible){if(a.aoColumns[b].sTitle!=c.innerHTML)c.innerHTML=a.aoColumns[b].sTitle}else{c.parentNode.removeChild(c);h++}}}else{f=p.createElement("tr");b=0;for(d=a.aoColumns.length;b<d;b++){c=a.aoColumns[b].nTh;c.innerHTML=a.aoColumns[b].sTitle;a.aoColumns[b].sClass!==null&&j(c).addClass(a.aoColumns[b].sClass);
+a.aoColumns[b].bVisible&&f.appendChild(c)}j(a.nTHead).html("")[0].appendChild(f)}if(a.bJUI){b=0;for(d=a.aoColumns.length;b<d;b++){c=a.aoColumns[b].nTh;f=p.createElement("div");f.className=a.oClasses.sSortJUIWrapper;j(c).contents().appendTo(f);f.appendChild(p.createElement("span"));c.appendChild(f)}}d=function(){this.onselectstart=function(){return false};return false};if(a.oFeatures.bSort)for(b=0;b<a.aoColumns.length;b++)if(a.aoColumns[b].bSortable!==false){ba(a,a.aoColumns[b].nTh,b);j(a.aoColumns[b].nTh).bind("mousedown.DT",
+d)}else j(a.aoColumns[b].nTh).addClass(a.oClasses.sSortableNone);if(a.nTFoot!==null){h=0;i=a.nTFoot.getElementsByTagName("tr");c=i[0].getElementsByTagName("th");b=0;for(d=c.length;b<d;b++)if(typeof a.aoColumns[b]!="undefined"){a.aoColumns[b].nTf=c[b-h];if(a.oClasses.sFooterTH!=="")a.aoColumns[b].nTf.className+=" "+a.oClasses.sFooterTH;f=1;for(e=i.length;f<e;f++){k=j(i[f]).children();a.aoColumns[b].anTfExtra.push(k[b-h]);a.aoColumns[b].bVisible||i[f].removeChild(k[b-h])}if(!a.aoColumns[b].bVisible){c[b-
+h].parentNode.removeChild(c[b-h]);h++}}}}function C(a){var b,c,d=[],f=0,e=false;b=a.asStripClasses.length;c=a.aoOpenRows.length;a.bDrawing=true;if(typeof a.iInitDisplayStart!="undefined"&&a.iInitDisplayStart!=-1){a._iDisplayStart=a.oFeatures.bServerSide?a.iInitDisplayStart:a.iInitDisplayStart>=a.fnRecordsDisplay()?0:a.iInitDisplayStart;a.iInitDisplayStart=-1;E(a)}if(!(!a.bDestroying&&a.oFeatures.bServerSide&&!ta(a))){a.oFeatures.bServerSide||a.iDraw++;if(a.aiDisplay.length!==0){var i=a._iDisplayStart,
+h=a._iDisplayEnd;if(a.oFeatures.bServerSide){i=0;h=a.aoData.length}for(i=i;i<h;i++){var k=a.aoData[a.aiDisplay[i]],l=k.nTr;if(b!==0){var q=a.asStripClasses[f%b];if(k._sRowStripe!=q){j(l).removeClass(k._sRowStripe).addClass(q);k._sRowStripe=q}}if(typeof a.fnRowCallback=="function"){l=a.fnRowCallback.call(a.oInstance,l,a.aoData[a.aiDisplay[i]]._aData,f,i);if(!l&&!e){H(a,0,"A node was not returned by fnRowCallback");e=true}}d.push(l);f++;if(c!==0)for(k=0;k<c;k++)l==a.aoOpenRows[k].nParent&&d.push(a.aoOpenRows[k].nTr)}}else{d[0]=
+p.createElement("tr");if(typeof a.asStripClasses[0]!="undefined")d[0].className=a.asStripClasses[0];e=p.createElement("td");e.setAttribute("valign","top");e.colSpan=S(a);e.className=a.oClasses.sRowEmpty;e.innerHTML=typeof a.oLanguage.sEmptyTable!="undefined"&&a.fnRecordsTotal()===0?a.oLanguage.sEmptyTable:a.oLanguage.sZeroRecords.replace("_MAX_",a.fnFormatNumber(a.fnRecordsTotal()));d[f].appendChild(e)}typeof a.fnHeaderCallback=="function"&&a.fnHeaderCallback.call(a.oInstance,j(">tr",a.nTHead)[0],
+V(a),a._iDisplayStart,a.fnDisplayEnd(),a.aiDisplay);typeof a.fnFooterCallback=="function"&&a.fnFooterCallback.call(a.oInstance,j(">tr",a.nTFoot)[0],V(a),a._iDisplayStart,a.fnDisplayEnd(),a.aiDisplay);f=p.createDocumentFragment();b=p.createDocumentFragment();if(a.nTBody){e=a.nTBody.parentNode;b.appendChild(a.nTBody);if(!a.oScroll.bInfinite||!a._bInitComplete||a.bSorted||a.bFiltered){c=a.nTBody.childNodes;for(b=c.length-1;b>=0;b--)c[b].parentNode.removeChild(c[b])}b=0;for(c=d.length;b<c;b++)f.appendChild(d[b]);
+a.nTBody.appendChild(f);e!==null&&e.appendChild(a.nTBody)}for(b=a.aoDrawCallback.length-1;b>=0;b--)a.aoDrawCallback[b].fn.call(a.oInstance,a);a.bSorted=false;a.bFiltered=false;a.bDrawing=false;if(a.oFeatures.bServerSide){K(a,false);typeof a._bInitComplete=="undefined"&&w(a)}}}function W(a){if(a.oFeatures.bSort)O(a,a.oPreviousSearch);else if(a.oFeatures.bFilter)P(a,a.oPreviousSearch);else{E(a);C(a)}}function ta(a){if(a.bAjaxDataGet){K(a,true);var b=a.aoColumns.length,c=[],d;a.iDraw++;c.push({name:"sEcho",
+value:a.iDraw});c.push({name:"iColumns",value:b});c.push({name:"sColumns",value:ca(a)});c.push({name:"iDisplayStart",value:a._iDisplayStart});c.push({name:"iDisplayLength",value:a.oFeatures.bPaginate!==false?a._iDisplayLength:-1});if(a.oFeatures.bFilter!==false){c.push({name:"sSearch",value:a.oPreviousSearch.sSearch});c.push({name:"bRegex",value:a.oPreviousSearch.bRegex});for(d=0;d<b;d++){c.push({name:"sSearch_"+d,value:a.aoPreSearchCols[d].sSearch});c.push({name:"bRegex_"+d,value:a.aoPreSearchCols[d].bRegex});
+c.push({name:"bSearchable_"+d,value:a.aoColumns[d].bSearchable})}}if(a.oFeatures.bSort!==false){var f=a.aaSortingFixed!==null?a.aaSortingFixed.length:0,e=a.aaSorting.length;c.push({name:"iSortingCols",value:f+e});for(d=0;d<f;d++){c.push({name:"iSortCol_"+d,value:a.aaSortingFixed[d][0]});c.push({name:"sSortDir_"+d,value:a.aaSortingFixed[d][1]})}for(d=0;d<e;d++){c.push({name:"iSortCol_"+(d+f),value:a.aaSorting[d][0]});c.push({name:"sSortDir_"+(d+f),value:a.aaSorting[d][1]})}for(d=0;d<b;d++)c.push({name:"bSortable_"+
+d,value:a.aoColumns[d].bSortable})}a.fnServerData.call(a.oInstance,a.sAjaxSource,c,function(i){ua(a,i)});return false}else return true}function ua(a,b){if(typeof b.sEcho!="undefined")if(b.sEcho*1<a.iDraw)return;else a.iDraw=b.sEcho*1;if(!a.oScroll.bInfinite||a.oScroll.bInfinite&&(a.bSorted||a.bFiltered))da(a);a._iRecordsTotal=b.iTotalRecords;a._iRecordsDisplay=b.iTotalDisplayRecords;var c=ca(a);if(c=typeof b.sColumns!="undefined"&&c!==""&&b.sColumns!=c)var d=va(a,b.sColumns);for(var f=0,e=b.aaData.length;f<
+e;f++)if(c){for(var i=[],h=0,k=a.aoColumns.length;h<k;h++)i.push(b.aaData[f][d[h]]);u(a,i)}else u(a,b.aaData[f]);a.aiDisplay=a.aiDisplayMaster.slice();a.bAjaxDataGet=false;C(a);a.bAjaxDataGet=true;K(a,false)}function sa(a){var b=p.createElement("div");a.nTable.parentNode.insertBefore(b,a.nTable);a.nTableWrapper=p.createElement("div");a.nTableWrapper.className=a.oClasses.sWrapper;a.sTableId!==""&&a.nTableWrapper.setAttribute("id",a.sTableId+"_wrapper");for(var c=a.nTableWrapper,d=a.sDom.split(""),
+f,e,i,h,k,l,q,t=0;t<d.length;t++){e=0;i=d[t];if(i=="<"){h=p.createElement("div");k=d[t+1];if(k=="'"||k=='"'){l="";for(q=2;d[t+q]!=k;){l+=d[t+q];q++}if(l=="H")l="fg-toolbar ui-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix";else if(l=="F")l="fg-toolbar ui-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix";if(l.indexOf(".")!=-1){k=l.split(".");h.setAttribute("id",k[0].substr(1,k[0].length-1));h.className=k[1]}else if(l.charAt(0)=="#")h.setAttribute("id",l.substr(1,
+l.length-1));else h.className=l;t+=q}c.appendChild(h);c=h}else if(i==">")c=c.parentNode;else if(i=="l"&&a.oFeatures.bPaginate&&a.oFeatures.bLengthChange){f=wa(a);e=1}else if(i=="f"&&a.oFeatures.bFilter){f=xa(a);e=1}else if(i=="r"&&a.oFeatures.bProcessing){f=ya(a);e=1}else if(i=="t"){f=za(a);e=1}else if(i=="i"&&a.oFeatures.bInfo){f=Aa(a);e=1}else if(i=="p"&&a.oFeatures.bPaginate){f=Ba(a);e=1}else if(n.aoFeatures.length!==0){h=n.aoFeatures;q=0;for(k=h.length;q<k;q++)if(i==h[q].cFeature){if(f=h[q].fnInit(a))e=
+1;break}}if(e==1&&f!==null){if(typeof a.aanFeatures[i]!="object")a.aanFeatures[i]=[];a.aanFeatures[i].push(f);c.appendChild(f)}}b.parentNode.replaceChild(a.nTableWrapper,b)}function za(a){if(a.oScroll.sX===""&&a.oScroll.sY==="")return a.nTable;var b=p.createElement("div"),c=p.createElement("div"),d=p.createElement("div"),f=p.createElement("div"),e=p.createElement("div"),i=p.createElement("div"),h=a.nTable.cloneNode(false),k=a.nTable.cloneNode(false),l=a.nTable.getElementsByTagName("thead")[0],q=a.nTable.getElementsByTagName("tfoot").length===
+0?null:a.nTable.getElementsByTagName("tfoot")[0],t=typeof g.bJQueryUI!="undefined"&&g.bJQueryUI?n.oJUIClasses:n.oStdClasses;c.appendChild(d);e.appendChild(i);f.appendChild(a.nTable);b.appendChild(c);b.appendChild(f);d.appendChild(h);h.appendChild(l);if(q!==null){b.appendChild(e);i.appendChild(k);k.appendChild(q)}b.className=t.sScrollWrapper;c.className=t.sScrollHead;d.className=t.sScrollHeadInner;f.className=t.sScrollBody;e.className=t.sScrollFoot;i.className=t.sScrollFootInner;if(a.oScroll.bAutoCss){c.style.overflow=
+"hidden";c.style.position="relative";e.style.overflow="hidden";f.style.overflow="auto"}c.style.border="0";c.style.width="100%";e.style.border="0";d.style.width="150%";h.removeAttribute("id");h.style.marginLeft="0";a.nTable.style.marginLeft="0";if(q!==null){k.removeAttribute("id");k.style.marginLeft="0"}d=j(">caption",a.nTable);i=0;for(k=d.length;i<k;i++)h.appendChild(d[i]);if(a.oScroll.sX!==""){c.style.width=v(a.oScroll.sX);f.style.width=v(a.oScroll.sX);if(q!==null)e.style.width=v(a.oScroll.sX);j(f).scroll(function(){c.scrollLeft=
+this.scrollLeft;if(q!==null)e.scrollLeft=this.scrollLeft})}if(a.oScroll.sY!=="")f.style.height=v(a.oScroll.sY);a.aoDrawCallback.push({fn:Ca,sName:"scrolling"});a.oScroll.bInfinite&&j(f).scroll(function(){if(!a.bDrawing)if(j(this).scrollTop()+j(this).height()>j(a.nTable).height()-a.oScroll.iLoadGap)if(a.fnDisplayEnd()<a.fnRecordsDisplay()){ea(a,"next");E(a);C(a)}});a.nScrollHead=c;a.nScrollFoot=e;return b}function Ca(a){var b=a.nScrollHead.getElementsByTagName("div")[0],c=b.getElementsByTagName("table")[0],
+d=a.nTable.parentNode,f,e,i,h,k,l,q,t,G=[];i=a.nTable.getElementsByTagName("thead");i.length>0&&a.nTable.removeChild(i[0]);if(a.nTFoot!==null){k=a.nTable.getElementsByTagName("tfoot");k.length>0&&a.nTable.removeChild(k[0])}i=a.nTHead.cloneNode(true);a.nTable.insertBefore(i,a.nTable.childNodes[0]);if(a.nTFoot!==null){k=a.nTFoot.cloneNode(true);a.nTable.insertBefore(k,a.nTable.childNodes[1])}var J=fa(i);f=0;for(e=J.length;f<e;f++){q=ga(a,f);J[f].style.width=a.aoColumns[q].sWidth}a.nTFoot!==null&&L(function(B){B.style.width=
+""},k.getElementsByTagName("tr"));f=j(a.nTable).outerWidth();if(a.oScroll.sX===""){a.nTable.style.width="100%";if(j.browser.msie&&j.browser.version<=7)a.nTable.style.width=v(j(a.nTable).outerWidth()-a.oScroll.iBarWidth)}else if(a.oScroll.sXInner!=="")a.nTable.style.width=v(a.oScroll.sXInner);else if(f==j(d).width()&&j(d).height()<j(a.nTable).height()){a.nTable.style.width=v(f-a.oScroll.iBarWidth);if(j(a.nTable).outerWidth()>f-a.oScroll.iBarWidth)a.nTable.style.width=v(f)}else a.nTable.style.width=
+v(f);f=j(a.nTable).outerWidth();e=a.nTHead.getElementsByTagName("tr");i=i.getElementsByTagName("tr");L(function(B,I){l=B.style;l.paddingTop="0";l.paddingBottom="0";l.borderTopWidth="0";l.borderBottomWidth="0";l.height=0;t=j(B).width();I.style.width=v(t);G.push(t)},i,e);j(i).height(0);if(a.nTFoot!==null){h=k.getElementsByTagName("tr");k=a.nTFoot.getElementsByTagName("tr");L(function(B,I){l=B.style;l.paddingTop="0";l.paddingBottom="0";l.borderTopWidth="0";l.borderBottomWidth="0";l.height=0;t=j(B).width();
+I.style.width=v(t);G.push(t)},h,k);j(h).height(0)}L(function(B){B.innerHTML="";B.style.width=v(G.shift())},i);a.nTFoot!==null&&L(function(B){B.innerHTML="";B.style.width=v(G.shift())},h);if(j(a.nTable).outerWidth()<f)if(a.oScroll.sX==="")H(a,1,"The table cannot fit into the current element which will cause column misalignment. It is suggested that you enable x-scrolling or increase the width the table has in which to be drawn");else a.oScroll.sXInner!==""&&H(a,1,"The table cannot fit into the current element which will cause column misalignment. It is suggested that you increase the sScrollXInner property to allow it to draw in a larger area, or simply remove that parameter to allow automatic calculation");
+if(a.oScroll.sY==="")if(j.browser.msie&&j.browser.version<=7)d.style.height=v(a.nTable.offsetHeight+a.oScroll.iBarWidth);if(a.oScroll.sY!==""&&a.oScroll.bCollapse){d.style.height=v(a.oScroll.sY);h=a.oScroll.sX!==""&&a.nTable.offsetWidth>d.offsetWidth?a.oScroll.iBarWidth:0;if(a.nTable.offsetHeight<d.offsetHeight)d.style.height=v(j(a.nTable).height()+h)}h=j(a.nTable).outerWidth();c.style.width=v(h);b.style.width=v(h+a.oScroll.iBarWidth);if(a.nTFoot!==null){b=a.nScrollFoot.getElementsByTagName("div")[0];
+c=b.getElementsByTagName("table")[0];b.style.width=v(a.nTable.offsetWidth+a.oScroll.iBarWidth);c.style.width=v(a.nTable.offsetWidth)}if(a.bSorted||a.bFiltered)d.scrollTop=0}function X(a){if(a.oFeatures.bAutoWidth===false)return false;$(a);for(var b=0,c=a.aoColumns.length;b<c;b++)a.aoColumns[b].nTh.style.width=a.aoColumns[b].sWidth}function xa(a){var b=p.createElement("div");a.sTableId!==""&&typeof a.aanFeatures.f=="undefined"&&b.setAttribute("id",a.sTableId+"_filter");b.className=a.oClasses.sFilter;
+b.innerHTML=a.oLanguage.sSearch+(a.oLanguage.sSearch===""?"":" ")+'<input type="text" />';var c=j("input",b);c.val(a.oPreviousSearch.sSearch.replace('"',"&quot;"));c.bind("keyup.DT",function(){for(var d=a.aanFeatures.f,f=0,e=d.length;f<e;f++)d[f]!=this.parentNode&&j("input",d[f]).val(this.value);this.value!=a.oPreviousSearch.sSearch&&P(a,{sSearch:this.value,bRegex:a.oPreviousSearch.bRegex,bSmart:a.oPreviousSearch.bSmart})});c.bind("keypress.DT",function(d){if(d.keyCode==13)return false});return b}
+function P(a,b,c){Da(a,b.sSearch,c,b.bRegex,b.bSmart);for(b=0;b<a.aoPreSearchCols.length;b++)Ea(a,a.aoPreSearchCols[b].sSearch,b,a.aoPreSearchCols[b].bRegex,a.aoPreSearchCols[b].bSmart);n.afnFiltering.length!==0&&Fa(a);a.bFiltered=true;a._iDisplayStart=0;E(a);C(a);ha(a,0)}function Fa(a){for(var b=n.afnFiltering,c=0,d=b.length;c<d;c++)for(var f=0,e=0,i=a.aiDisplay.length;e<i;e++){var h=a.aiDisplay[e-f];if(!b[c](a,a.aoData[h]._aData,h)){a.aiDisplay.splice(e-f,1);f++}}}function Ea(a,b,c,d,f){if(b!==
+""){var e=0;b=ia(b,d,f);for(d=a.aiDisplay.length-1;d>=0;d--){f=ja(a.aoData[a.aiDisplay[d]]._aData[c],a.aoColumns[c].sType);if(!b.test(f)){a.aiDisplay.splice(d,1);e++}}}}function Da(a,b,c,d,f){var e=ia(b,d,f);if(typeof c=="undefined"||c===null)c=0;if(n.afnFiltering.length!==0)c=1;if(b.length<=0){a.aiDisplay.splice(0,a.aiDisplay.length);a.aiDisplay=a.aiDisplayMaster.slice()}else if(a.aiDisplay.length==a.aiDisplayMaster.length||a.oPreviousSearch.sSearch.length>b.length||c==1||b.indexOf(a.oPreviousSearch.sSearch)!==
+0){a.aiDisplay.splice(0,a.aiDisplay.length);ha(a,1);for(c=0;c<a.aiDisplayMaster.length;c++)e.test(a.asDataSearch[c])&&a.aiDisplay.push(a.aiDisplayMaster[c])}else{var i=0;for(c=0;c<a.asDataSearch.length;c++)if(!e.test(a.asDataSearch[c])){a.aiDisplay.splice(c-i,1);i++}}a.oPreviousSearch.sSearch=b;a.oPreviousSearch.bRegex=d;a.oPreviousSearch.bSmart=f}function ha(a,b){a.asDataSearch.splice(0,a.asDataSearch.length);b=typeof b!="undefined"&&b==1?a.aiDisplayMaster:a.aiDisplay;for(var c=0,d=b.length;c<d;c++)a.asDataSearch[c]=
+ka(a,a.aoData[b[c]]._aData)}function ka(a,b){for(var c="",d=p.createElement("div"),f=0,e=a.aoColumns.length;f<e;f++)if(a.aoColumns[f].bSearchable)c+=ja(b[f],a.aoColumns[f].sType)+"  ";if(c.indexOf("&")!==-1){d.innerHTML=c;c=d.textContent?d.textContent:d.innerText;c=c.replace(/\n/g," ").replace(/\r/g,"")}return c}function ia(a,b,c){if(c){a=b?a.split(" "):la(a).split(" ");a="^(?=.*?"+a.join(")(?=.*?")+").*$";return new RegExp(a,"i")}else{a=b?a:la(a);return new RegExp(a,"i")}}function ja(a,b){if(typeof n.ofnSearch[b]==
+"function")return n.ofnSearch[b](a);else if(b=="html")return a.replace(/\n/g," ").replace(/<.*?>/g,"");else if(typeof a=="string")return a.replace(/\n/g," ");return a}function O(a,b){var c,d,f,e,i,h,k=[],l=[],q=n.oSort,t=a.aoData,G=a.aoColumns;if(!a.oFeatures.bServerSide&&(a.aaSorting.length!==0||a.aaSortingFixed!==null)){k=a.aaSortingFixed!==null?a.aaSortingFixed.concat(a.aaSorting):a.aaSorting.slice();for(f=0;f<k.length;f++){e=k[f][0];i=M(a,e);h=a.aoColumns[e].sSortDataType;if(typeof n.afnSortData[h]!=
+"undefined"){var J=n.afnSortData[h](a,e,i);i=0;for(h=t.length;i<h;i++)t[i]._aData[e]=J[i]}}f=0;for(e=a.aiDisplayMaster.length;f<e;f++)l[a.aiDisplayMaster[f]]=f;var B=k.length;a.aiDisplayMaster.sort(function(I,Y){var N;for(f=0;f<B;f++){c=G[k[f][0]].iDataSort;d=G[c].sType;N=q[d+"-"+k[f][1]](t[I]._aData[c],t[Y]._aData[c]);if(N!==0)return N}return q["numeric-asc"](l[I],l[Y])})}if(typeof b=="undefined"||b)T(a);a.bSorted=true;if(a.oFeatures.bFilter)P(a,a.oPreviousSearch,1);else{a.aiDisplay=a.aiDisplayMaster.slice();
+a._iDisplayStart=0;E(a);C(a)}}function ba(a,b,c,d){j(b).bind("click.DT",function(f){if(a.aoColumns[c].bSortable!==false){var e=function(){var i,h;if(f.shiftKey){for(var k=false,l=0;l<a.aaSorting.length;l++)if(a.aaSorting[l][0]==c){k=true;i=a.aaSorting[l][0];h=a.aaSorting[l][2]+1;if(typeof a.aoColumns[i].asSorting[h]=="undefined")a.aaSorting.splice(l,1);else{a.aaSorting[l][1]=a.aoColumns[i].asSorting[h];a.aaSorting[l][2]=h}break}k===false&&a.aaSorting.push([c,a.aoColumns[c].asSorting[0],0])}else if(a.aaSorting.length==
+1&&a.aaSorting[0][0]==c){i=a.aaSorting[0][0];h=a.aaSorting[0][2]+1;if(typeof a.aoColumns[i].asSorting[h]=="undefined")h=0;a.aaSorting[0][1]=a.aoColumns[i].asSorting[h];a.aaSorting[0][2]=h}else{a.aaSorting.splice(0,a.aaSorting.length);a.aaSorting.push([c,a.aoColumns[c].asSorting[0],0])}O(a)};if(a.oFeatures.bProcessing){K(a,true);setTimeout(function(){e();a.oFeatures.bServerSide||K(a,false)},0)}else e();typeof d=="function"&&d(a)}})}function T(a){var b,c,d,f,e,i=a.aoColumns.length,h=a.oClasses;for(b=
+0;b<i;b++)a.aoColumns[b].bSortable&&j(a.aoColumns[b].nTh).removeClass(h.sSortAsc+" "+h.sSortDesc+" "+a.aoColumns[b].sSortingClass);f=a.aaSortingFixed!==null?a.aaSortingFixed.concat(a.aaSorting):a.aaSorting.slice();for(b=0;b<a.aoColumns.length;b++)if(a.aoColumns[b].bSortable){e=a.aoColumns[b].sSortingClass;d=-1;for(c=0;c<f.length;c++)if(f[c][0]==b){e=f[c][1]=="asc"?h.sSortAsc:h.sSortDesc;d=c;break}j(a.aoColumns[b].nTh).addClass(e);if(a.bJUI){c=j("span",a.aoColumns[b].nTh);c.removeClass(h.sSortJUIAsc+
+" "+h.sSortJUIDesc+" "+h.sSortJUI+" "+h.sSortJUIAscAllowed+" "+h.sSortJUIDescAllowed);c.addClass(d==-1?a.aoColumns[b].sSortingClassJUI:f[d][1]=="asc"?h.sSortJUIAsc:h.sSortJUIDesc)}}else j(a.aoColumns[b].nTh).addClass(a.aoColumns[b].sSortingClass);e=h.sSortColumn;if(a.oFeatures.bSort&&a.oFeatures.bSortClasses){d=Z(a);if(d.length>=i)for(b=0;b<i;b++)if(d[b].className.indexOf(e+"1")!=-1){c=0;for(a=d.length/i;c<a;c++)d[i*c+b].className=j.trim(d[i*c+b].className.replace(e+"1",""))}else if(d[b].className.indexOf(e+
+"2")!=-1){c=0;for(a=d.length/i;c<a;c++)d[i*c+b].className=j.trim(d[i*c+b].className.replace(e+"2",""))}else if(d[b].className.indexOf(e+"3")!=-1){c=0;for(a=d.length/i;c<a;c++)d[i*c+b].className=j.trim(d[i*c+b].className.replace(" "+e+"3",""))}h=1;var k;for(b=0;b<f.length;b++){k=parseInt(f[b][0],10);c=0;for(a=d.length/i;c<a;c++)d[i*c+k].className+=" "+e+h;h<3&&h++}}}function Ba(a){if(a.oScroll.bInfinite)return null;var b=p.createElement("div");b.className=a.oClasses.sPaging+a.sPaginationType;n.oPagination[a.sPaginationType].fnInit(a,
+b,function(c){E(c);C(c)});typeof a.aanFeatures.p=="undefined"&&a.aoDrawCallback.push({fn:function(c){n.oPagination[c.sPaginationType].fnUpdate(c,function(d){E(d);C(d)})},sName:"pagination"});return b}function ea(a,b){var c=a._iDisplayStart;if(b=="first")a._iDisplayStart=0;else if(b=="previous"){a._iDisplayStart=a._iDisplayLength>=0?a._iDisplayStart-a._iDisplayLength:0;if(a._iDisplayStart<0)a._iDisplayStart=0}else if(b=="next")if(a._iDisplayLength>=0){if(a._iDisplayStart+a._iDisplayLength<a.fnRecordsDisplay())a._iDisplayStart+=
+a._iDisplayLength}else a._iDisplayStart=0;else if(b=="last")if(a._iDisplayLength>=0){b=parseInt((a.fnRecordsDisplay()-1)/a._iDisplayLength,10)+1;a._iDisplayStart=(b-1)*a._iDisplayLength}else a._iDisplayStart=0;else H(a,0,"Unknown paging action: "+b);return c!=a._iDisplayStart}function Aa(a){var b=p.createElement("div");b.className=a.oClasses.sInfo;if(typeof a.aanFeatures.i=="undefined"){a.aoDrawCallback.push({fn:Ga,sName:"information"});a.sTableId!==""&&b.setAttribute("id",a.sTableId+"_info")}return b}
+function Ga(a){if(!(!a.oFeatures.bInfo||a.aanFeatures.i.length===0)){var b=a._iDisplayStart+1,c=a.fnDisplayEnd(),d=a.fnRecordsTotal(),f=a.fnRecordsDisplay(),e=a.fnFormatNumber(b),i=a.fnFormatNumber(c),h=a.fnFormatNumber(d),k=a.fnFormatNumber(f);if(a.oScroll.bInfinite)e=a.fnFormatNumber(1);e=a.fnRecordsDisplay()===0&&a.fnRecordsDisplay()==a.fnRecordsTotal()?a.oLanguage.sInfoEmpty+a.oLanguage.sInfoPostFix:a.fnRecordsDisplay()===0?a.oLanguage.sInfoEmpty+" "+a.oLanguage.sInfoFiltered.replace("_MAX_",
+h)+a.oLanguage.sInfoPostFix:a.fnRecordsDisplay()==a.fnRecordsTotal()?a.oLanguage.sInfo.replace("_START_",e).replace("_END_",i).replace("_TOTAL_",k)+a.oLanguage.sInfoPostFix:a.oLanguage.sInfo.replace("_START_",e).replace("_END_",i).replace("_TOTAL_",k)+" "+a.oLanguage.sInfoFiltered.replace("_MAX_",a.fnFormatNumber(a.fnRecordsTotal()))+a.oLanguage.sInfoPostFix;if(a.oLanguage.fnInfoCallback!==null)e=a.oLanguage.fnInfoCallback(a,b,c,d,f,e);a=a.aanFeatures.i;b=0;for(c=a.length;b<c;b++)j(a[b]).html(e)}}
+function wa(a){if(a.oScroll.bInfinite)return null;var b='<select size="1" '+(a.sTableId===""?"":'name="'+a.sTableId+'_length"')+">",c,d;if(a.aLengthMenu.length==2&&typeof a.aLengthMenu[0]=="object"&&typeof a.aLengthMenu[1]=="object"){c=0;for(d=a.aLengthMenu[0].length;c<d;c++)b+='<option value="'+a.aLengthMenu[0][c]+'">'+a.aLengthMenu[1][c]+"</option>"}else{c=0;for(d=a.aLengthMenu.length;c<d;c++)b+='<option value="'+a.aLengthMenu[c]+'">'+a.aLengthMenu[c]+"</option>"}b+="</select>";var f=p.createElement("div");
+a.sTableId!==""&&typeof a.aanFeatures.l=="undefined"&&f.setAttribute("id",a.sTableId+"_length");f.className=a.oClasses.sLength;f.innerHTML=a.oLanguage.sLengthMenu.replace("_MENU_",b);j('select option[value="'+a._iDisplayLength+'"]',f).attr("selected",true);j("select",f).bind("change.DT",function(){var e=j(this).val(),i=a.aanFeatures.l;c=0;for(d=i.length;c<d;c++)i[c]!=this.parentNode&&j("select",i[c]).val(e);a._iDisplayLength=parseInt(e,10);E(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;C(a)});return f}function ya(a){var b=p.createElement("div");a.sTableId!==""&&typeof a.aanFeatures.r=="undefined"&&b.setAttribute("id",a.sTableId+"_processing");b.innerHTML=a.oLanguage.sProcessing;b.className=a.oClasses.sProcessing;a.nTable.parentNode.insertBefore(b,a.nTable);return b}function K(a,b){if(a.oFeatures.bProcessing){a=a.aanFeatures.r;for(var c=0,d=a.length;c<d;c++)a[c].style.visibility=
+b?"visible":"hidden"}}function ga(a,b){for(var c=-1,d=0;d<a.aoColumns.length;d++){a.aoColumns[d].bVisible===true&&c++;if(c==b)return d}return null}function M(a,b){for(var c=-1,d=0;d<a.aoColumns.length;d++){a.aoColumns[d].bVisible===true&&c++;if(d==b)return a.aoColumns[d].bVisible===true?c:null}return null}function Q(a,b){var c,d;c=a._iDisplayStart;for(d=a._iDisplayEnd;c<d;c++)if(a.aoData[a.aiDisplay[c]].nTr==b)return a.aiDisplay[c];c=0;for(d=a.aoData.length;c<d;c++)if(a.aoData[c].nTr==b)return c;
+return null}function S(a){for(var b=0,c=0;c<a.aoColumns.length;c++)a.aoColumns[c].bVisible===true&&b++;return b}function E(a){a._iDisplayEnd=a.oFeatures.bPaginate===false?a.aiDisplay.length:a._iDisplayStart+a._iDisplayLength>a.aiDisplay.length||a._iDisplayLength==-1?a.aiDisplay.length:a._iDisplayStart+a._iDisplayLength}function Ha(a,b){if(!a||a===null||a==="")return 0;if(typeof b=="undefined")b=p.getElementsByTagName("body")[0];var c=p.createElement("div");c.style.width=a;b.appendChild(c);a=c.offsetWidth;
+b.removeChild(c);return a}function $(a){var b=0,c,d=0,f=a.aoColumns.length,e,i=j("th",a.nTHead);for(e=0;e<f;e++)if(a.aoColumns[e].bVisible){d++;if(a.aoColumns[e].sWidth!==null){c=Ha(a.aoColumns[e].sWidthOrig,a.nTable.parentNode);if(c!==null)a.aoColumns[e].sWidth=v(c);b++}}if(f==i.length&&b===0&&d==f&&a.oScroll.sX===""&&a.oScroll.sY==="")for(e=0;e<a.aoColumns.length;e++){c=j(i[e]).width();if(c!==null)a.aoColumns[e].sWidth=v(c)}else{b=a.nTable.cloneNode(false);e=p.createElement("tbody");c=p.createElement("tr");
+b.removeAttribute("id");b.appendChild(a.nTHead.cloneNode(true));if(a.nTFoot!==null){b.appendChild(a.nTFoot.cloneNode(true));L(function(h){h.style.width=""},b.getElementsByTagName("tr"))}b.appendChild(e);e.appendChild(c);e=j("thead th",b);if(e.length===0)e=j("tbody tr:eq(0)>td",b);e.each(function(h){this.style.width="";h=ga(a,h);if(h!==null&&a.aoColumns[h].sWidthOrig!=="")this.style.width=a.aoColumns[h].sWidthOrig});for(e=0;e<f;e++)if(a.aoColumns[e].bVisible){d=Ia(a,e);if(d!==null){d=d.cloneNode(true);
+c.appendChild(d)}}e=a.nTable.parentNode;e.appendChild(b);if(a.oScroll.sX!==""&&a.oScroll.sXInner!=="")b.style.width=v(a.oScroll.sXInner);else if(a.oScroll.sX!==""){b.style.width="";if(j(b).width()<e.offsetWidth)b.style.width=v(e.offsetWidth)}else if(a.oScroll.sY!=="")b.style.width=v(e.offsetWidth);b.style.visibility="hidden";Ja(a,b);f=j("tbody tr:eq(0)>td",b);if(f.length===0)f=j("thead tr:eq(0)>th",b);for(e=c=0;e<a.aoColumns.length;e++)if(a.aoColumns[e].bVisible){d=j(f[c]).outerWidth();if(d!==null&&
+d>0)a.aoColumns[e].sWidth=v(d);c++}a.nTable.style.width=v(j(b).outerWidth());b.parentNode.removeChild(b)}}function Ja(a,b){if(a.oScroll.sX===""&&a.oScroll.sY!==""){j(b).width();b.style.width=v(j(b).outerWidth()-a.oScroll.iBarWidth)}else if(a.oScroll.sX!=="")b.style.width=v(j(b).outerWidth())}function Ia(a,b,c){if(typeof c=="undefined"||c){c=Ka(a,b);b=M(a,b);if(c<0)return null;return a.aoData[c].nTr.getElementsByTagName("td")[b]}var d=-1,f,e;c=-1;var i=p.createElement("div");i.style.visibility="hidden";
+i.style.position="absolute";p.body.appendChild(i);f=0;for(e=a.aoData.length;f<e;f++){i.innerHTML=a.aoData[f]._aData[b];if(i.offsetWidth>d){d=i.offsetWidth;c=f}}p.body.removeChild(i);if(c>=0){b=M(a,b);if(a=a.aoData[c].nTr.getElementsByTagName("td")[b])return a}return null}function Ka(a,b){for(var c=-1,d=-1,f=0;f<a.aoData.length;f++){var e=a.aoData[f]._aData[b];if(e.length>c){c=e.length;d=f}}return d}function v(a){if(a===null)return"0px";if(typeof a=="number"){if(a<0)return"0px";return a+"px"}var b=
+a.charCodeAt(a.length-1);if(b<48||b>57)return a;return a+"px"}function Oa(a,b){if(a.length!=b.length)return 1;for(var c=0;c<a.length;c++)if(a[c]!=b[c])return 2;return 0}function aa(a){for(var b=n.aTypes,c=b.length,d=0;d<c;d++){var f=b[d](a);if(f!==null)return f}return"string"}function A(a){for(var b=0;b<D.length;b++)if(D[b].nTable==a)return D[b];return null}function V(a){for(var b=[],c=a.aoData.length,d=0;d<c;d++)b.push(a.aoData[d]._aData);return b}function R(a){for(var b=[],c=a.aoData.length,d=0;d<
+c;d++)b.push(a.aoData[d].nTr);return b}function Z(a){var b=R(a),c=[],d,f=[],e,i,h,k;e=0;for(i=b.length;e<i;e++){c=[];h=0;for(k=b[e].childNodes.length;h<k;h++){d=b[e].childNodes[h];d.nodeName.toUpperCase()=="TD"&&c.push(d)}h=d=0;for(k=a.aoColumns.length;h<k;h++)if(a.aoColumns[h].bVisible)f.push(c[h-d]);else{f.push(a.aoData[e]._anHidden[h]);d++}}return f}function la(a){return a.replace(new RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^)","g"),"\\$1")}function ma(a,b){for(var c=
+-1,d=0,f=a.length;d<f;d++)if(a[d]==b)c=d;else a[d]>b&&a[d]--;c!=-1&&a.splice(c,1)}function va(a,b){b=b.split(",");for(var c=[],d=0,f=a.aoColumns.length;d<f;d++)for(var e=0;e<f;e++)if(a.aoColumns[d].sName==b[e]){c.push(e);break}return c}function ca(a){for(var b="",c=0,d=a.aoColumns.length;c<d;c++)b+=a.aoColumns[c].sName+",";if(b.length==d)return"";return b.slice(0,-1)}function H(a,b,c){a=a.sTableId===""?"DataTables warning: "+c:"DataTables warning (table id = '"+a.sTableId+"'): "+c;if(b===0)if(n.sErrMode==
+"alert")alert(a);else throw a;else typeof console!="undefined"&&typeof console.log!="undefined"&&console.log(a)}function da(a){a.aoData.splice(0,a.aoData.length);a.aiDisplayMaster.splice(0,a.aiDisplayMaster.length);a.aiDisplay.splice(0,a.aiDisplay.length);E(a)}function na(a){if(!(!a.oFeatures.bStateSave||typeof a.bDestroying!="undefined")){var b,c,d,f="{";f+='"iCreate":'+(new Date).getTime()+",";f+='"iStart":'+a._iDisplayStart+",";f+='"iEnd":'+a._iDisplayEnd+",";f+='"iLength":'+a._iDisplayLength+
+",";f+='"sFilter":"'+encodeURIComponent(a.oPreviousSearch.sSearch)+'",';f+='"sFilterEsc":'+!a.oPreviousSearch.bRegex+",";f+='"aaSorting":[ ';for(b=0;b<a.aaSorting.length;b++)f+="["+a.aaSorting[b][0]+',"'+a.aaSorting[b][1]+'"],';f=f.substring(0,f.length-1);f+="],";f+='"aaSearchCols":[ ';for(b=0;b<a.aoPreSearchCols.length;b++)f+='["'+encodeURIComponent(a.aoPreSearchCols[b].sSearch)+'",'+!a.aoPreSearchCols[b].bRegex+"],";f=f.substring(0,f.length-1);f+="],";f+='"abVisCols":[ ';for(b=0;b<a.aoColumns.length;b++)f+=
+a.aoColumns[b].bVisible+",";f=f.substring(0,f.length-1);f+="]";b=0;for(c=a.aoStateSave.length;b<c;b++){d=a.aoStateSave[b].fn(a,f);if(d!=="")f=d}f+="}";La(a.sCookiePrefix+a.sInstance,f,a.iCookieDuration,a.sCookiePrefix,a.fnCookieCallback)}}function Ma(a,b){if(a.oFeatures.bStateSave){var c,d,f;d=oa(a.sCookiePrefix+a.sInstance);if(d!==null&&d!==""){try{c=typeof j.parseJSON=="function"?j.parseJSON(d.replace(/'/g,'"')):eval("("+d+")")}catch(e){return}d=0;for(f=a.aoStateLoad.length;d<f;d++)if(!a.aoStateLoad[d].fn(a,
+c))return;a.oLoadedState=j.extend(true,{},c);a._iDisplayStart=c.iStart;a.iInitDisplayStart=c.iStart;a._iDisplayEnd=c.iEnd;a._iDisplayLength=c.iLength;a.oPreviousSearch.sSearch=decodeURIComponent(c.sFilter);a.aaSorting=c.aaSorting.slice();a.saved_aaSorting=c.aaSorting.slice();if(typeof c.sFilterEsc!="undefined")a.oPreviousSearch.bRegex=!c.sFilterEsc;if(typeof c.aaSearchCols!="undefined")for(d=0;d<c.aaSearchCols.length;d++)a.aoPreSearchCols[d]={sSearch:decodeURIComponent(c.aaSearchCols[d][0]),bRegex:!c.aaSearchCols[d][1]};
+if(typeof c.abVisCols!="undefined"){b.saved_aoColumns=[];for(d=0;d<c.abVisCols.length;d++){b.saved_aoColumns[d]={};b.saved_aoColumns[d].bVisible=c.abVisCols[d]}}}}}function La(a,b,c,d,f){var e=new Date;e.setTime(e.getTime()+c*1E3);c=ra.location.pathname.split("/");a=a+"_"+c.pop().replace(/[\/:]/g,"").toLowerCase();var i;if(f!==null){i=typeof j.parseJSON=="function"?j.parseJSON(b):eval("("+b+")");b=f(a,i,e.toGMTString(),c.join("/")+"/")}else b=a+"="+encodeURIComponent(b)+"; expires="+e.toGMTString()+
+"; path="+c.join("/")+"/";f="";e=9999999999999;if((oa(a)!==null?p.cookie.length:b.length+p.cookie.length)+10>4096){a=p.cookie.split(";");for(var h=0,k=a.length;h<k;h++)if(a[h].indexOf(d)!=-1){var l=a[h].split("=");try{i=eval("("+decodeURIComponent(l[1])+")")}catch(q){continue}if(typeof i.iCreate!="undefined"&&i.iCreate<e){f=l[0];e=i.iCreate}}if(f!=="")p.cookie=f+"=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path="+c.join("/")+"/"}p.cookie=b}function oa(a){var b=ra.location.pathname.split("/");a=a+"_"+
+b[b.length-1].replace(/[\/:]/g,"").toLowerCase()+"=";b=p.cookie.split(";");for(var c=0;c<b.length;c++){for(var d=b[c];d.charAt(0)==" ";)d=d.substring(1,d.length);if(d.indexOf(a)===0)return decodeURIComponent(d.substring(a.length,d.length))}return null}function fa(a){a=a.getElementsByTagName("tr");if(a.length==1)return a[0].getElementsByTagName("th");var b=[],c=[],d,f,e,i,h,k,l=function(I,Y,N){for(;typeof I[Y][N]!="undefined";)N++;return N},q=function(I){if(typeof b[I]=="undefined")b[I]=[]};d=0;for(i=
+a.length;d<i;d++){q(d);var t=0,G=[];f=0;for(h=a[d].childNodes.length;f<h;f++)if(a[d].childNodes[f].nodeName.toUpperCase()=="TD"||a[d].childNodes[f].nodeName.toUpperCase()=="TH")G.push(a[d].childNodes[f]);f=0;for(h=G.length;f<h;f++){var J=G[f].getAttribute("colspan")*1,B=G[f].getAttribute("rowspan")*1;if(!J||J===0||J===1){k=l(b,d,t);b[d][k]=G[f].nodeName.toUpperCase()=="TD"?4:G[f];if(B||B===0||B===1)for(e=1;e<B;e++){q(d+e);b[d+e][k]=2}t++}else{k=l(b,d,t);for(e=0;e<J;e++)b[d][k+e]=3;t+=J}}}d=0;for(i=
+b.length;d<i;d++){f=0;for(h=b[d].length;f<h;f++)if(typeof b[d][f]=="object"&&typeof c[f]=="undefined")c[f]=b[d][f]}return c}function Na(){var a=p.createElement("p"),b=a.style;b.width="100%";b.height="200px";var c=p.createElement("div");b=c.style;b.position="absolute";b.top="0px";b.left="0px";b.visibility="hidden";b.width="200px";b.height="150px";b.overflow="hidden";c.appendChild(a);p.body.appendChild(c);b=a.offsetWidth;c.style.overflow="scroll";a=a.offsetWidth;if(b==a)a=c.clientWidth;p.body.removeChild(c);
+return b-a}function L(a,b,c){for(var d=0,f=b.length;d<f;d++)for(var e=0,i=b[d].childNodes.length;e<i;e++)if(b[d].childNodes[e].nodeType==1)typeof c!="undefined"?a(b[d].childNodes[e],c[d].childNodes[e]):a(b[d].childNodes[e])}function o(a,b,c,d){if(typeof d=="undefined")d=c;if(typeof b[c]!="undefined")a[d]=b[c]}this.oApi={};this.fnDraw=function(a){var b=A(this[n.iApiIndex]);if(typeof a!="undefined"&&a===false){E(b);C(b)}else W(b)};this.fnFilter=function(a,b,c,d,f){var e=A(this[n.iApiIndex]);if(e.oFeatures.bFilter){if(typeof c==
+"undefined")c=false;if(typeof d=="undefined")d=true;if(typeof f=="undefined")f=true;if(typeof b=="undefined"||b===null){P(e,{sSearch:a,bRegex:c,bSmart:d},1);if(f&&typeof e.aanFeatures.f!="undefined"){b=e.aanFeatures.f;c=0;for(d=b.length;c<d;c++)j("input",b[c]).val(a)}}else{e.aoPreSearchCols[b].sSearch=a;e.aoPreSearchCols[b].bRegex=c;e.aoPreSearchCols[b].bSmart=d;P(e,e.oPreviousSearch,1)}}};this.fnSettings=function(){return A(this[n.iApiIndex])};this.fnVersionCheck=n.fnVersionCheck;this.fnSort=function(a){var b=
+A(this[n.iApiIndex]);b.aaSorting=a;O(b)};this.fnSortListener=function(a,b,c){ba(A(this[n.iApiIndex]),a,b,c)};this.fnAddData=function(a,b){if(a.length===0)return[];var c=[],d,f=A(this[n.iApiIndex]);if(typeof a[0]=="object")for(var e=0;e<a.length;e++){d=u(f,a[e]);if(d==-1)return c;c.push(d)}else{d=u(f,a);if(d==-1)return c;c.push(d)}f.aiDisplay=f.aiDisplayMaster.slice();if(typeof b=="undefined"||b)W(f);return c};this.fnDeleteRow=function(a,b,c){var d=A(this[n.iApiIndex]);a=typeof a=="object"?Q(d,a):
+a;var f=d.aoData.splice(a,1),e=j.inArray(a,d.aiDisplay);d.asDataSearch.splice(e,1);ma(d.aiDisplayMaster,a);ma(d.aiDisplay,a);typeof b=="function"&&b.call(this,d,f);if(d._iDisplayStart>=d.aiDisplay.length){d._iDisplayStart-=d._iDisplayLength;if(d._iDisplayStart<0)d._iDisplayStart=0}if(typeof c=="undefined"||c){E(d);C(d)}return f};this.fnClearTable=function(a){var b=A(this[n.iApiIndex]);da(b);if(typeof a=="undefined"||a)C(b)};this.fnOpen=function(a,b,c){var d=A(this[n.iApiIndex]);this.fnClose(a);var f=
+p.createElement("tr"),e=p.createElement("td");f.appendChild(e);e.className=c;e.colSpan=S(d);e.innerHTML=b;b=j("tr",d.nTBody);j.inArray(a,b)!=-1&&j(f).insertAfter(a);d.aoOpenRows.push({nTr:f,nParent:a});return f};this.fnClose=function(a){for(var b=A(this[n.iApiIndex]),c=0;c<b.aoOpenRows.length;c++)if(b.aoOpenRows[c].nParent==a){(a=b.aoOpenRows[c].nTr.parentNode)&&a.removeChild(b.aoOpenRows[c].nTr);b.aoOpenRows.splice(c,1);return 0}return 1};this.fnGetData=function(a){var b=A(this[n.iApiIndex]);if(typeof a!=
+"undefined"){a=typeof a=="object"?Q(b,a):a;return(aRowData=b.aoData[a])?aRowData._aData:null}return V(b)};this.fnGetNodes=function(a){var b=A(this[n.iApiIndex]);if(typeof a!="undefined")return(aRowData=b.aoData[a])?aRowData.nTr:null;return R(b)};this.fnGetPosition=function(a){var b=A(this[n.iApiIndex]);if(a.nodeName.toUpperCase()=="TR")return Q(b,a);else if(a.nodeName.toUpperCase()=="TD")for(var c=Q(b,a.parentNode),d=0,f=0;f<b.aoColumns.length;f++)if(b.aoColumns[f].bVisible){if(b.aoData[c].nTr.getElementsByTagName("td")[f-
+d]==a)return[c,f-d,f]}else d++;return null};this.fnUpdate=function(a,b,c,d,f){var e=A(this[n.iApiIndex]),i,h;b=typeof b=="object"?Q(e,b):b;if(typeof a!="object"){h=a;e.aoData[b]._aData[c]=h;if(e.aoColumns[c].fnRender!==null){h=e.aoColumns[c].fnRender({iDataRow:b,iDataColumn:c,aData:e.aoData[b]._aData,oSettings:e});if(e.aoColumns[c].bUseRendered)e.aoData[b]._aData[c]=h}i=M(e,c);if(i!==null)e.aoData[b].nTr.getElementsByTagName("td")[i].innerHTML=h;else e.aoData[b]._anHidden[c].innerHTML=h}else{if(a.length!=
+e.aoColumns.length){H(e,0,"An array passed to fnUpdate must have the same number of columns as the table in question - in this case "+e.aoColumns.length);return 1}for(c=0;c<a.length;c++){h=a[c];e.aoData[b]._aData[c]=h;if(e.aoColumns[c].fnRender!==null){h=e.aoColumns[c].fnRender({iDataRow:b,iDataColumn:c,aData:e.aoData[b]._aData,oSettings:e});if(e.aoColumns[c].bUseRendered)e.aoData[b]._aData[c]=h}i=M(e,c);if(i!==null)e.aoData[b].nTr.getElementsByTagName("td")[i].innerHTML=h;else e.aoData[b]._anHidden[c].innerHTML=
+h}}a=j.inArray(b,e.aiDisplay);e.asDataSearch[a]=ka(e,e.aoData[b]._aData);if(typeof f=="undefined"||f)X(e);if(typeof d=="undefined"||d)W(e);return 0};this.fnSetColumnVis=function(a,b,c){var d=A(this[n.iApiIndex]),f,e;e=d.aoColumns.length;var i,h,k,l,q;if(d.aoColumns[a].bVisible!=b){l=j(">tr",d.nTHead)[0];i=j(">tr",d.nTFoot)[0];q=[];h=[];for(f=0;f<e;f++){q.push(d.aoColumns[f].nTh);h.push(d.aoColumns[f].nTf)}if(b){for(f=b=0;f<a;f++)d.aoColumns[f].bVisible&&b++;if(b>=S(d)){l.appendChild(q[a]);l=j(">tr",
+d.nTHead);f=1;for(e=l.length;f<e;f++)l[f].appendChild(d.aoColumns[a].anThExtra[f-1]);if(i){i.appendChild(h[a]);l=j(">tr",d.nTFoot);f=1;for(e=l.length;f<e;f++)l[f].appendChild(d.aoColumns[a].anTfExtra[f-1])}f=0;for(e=d.aoData.length;f<e;f++){i=d.aoData[f]._anHidden[a];d.aoData[f].nTr.appendChild(i)}}else{for(f=a;f<e;f++){k=M(d,f);if(k!==null)break}l.insertBefore(q[a],l.getElementsByTagName("th")[k]);l=j(">tr",d.nTHead);f=1;for(e=l.length;f<e;f++){q=j(l[f]).children();l[f].insertBefore(d.aoColumns[a].anThExtra[f-
+1],q[k])}if(i){i.insertBefore(h[a],i.getElementsByTagName("th")[k]);l=j(">tr",d.nTFoot);f=1;for(e=l.length;f<e;f++){q=j(l[f]).children();l[f].insertBefore(d.aoColumns[a].anTfExtra[f-1],q[k])}}Z(d);f=0;for(e=d.aoData.length;f<e;f++){i=d.aoData[f]._anHidden[a];d.aoData[f].nTr.insertBefore(i,j(">td:eq("+k+")",d.aoData[f].nTr)[0])}}d.aoColumns[a].bVisible=true}else{l.removeChild(q[a]);f=0;for(e=d.aoColumns[a].anThExtra.length;f<e;f++){k=d.aoColumns[a].anThExtra[f];k.parentNode.removeChild(k)}if(i){i.removeChild(h[a]);
+f=0;for(e=d.aoColumns[a].anTfExtra.length;f<e;f++){k=d.aoColumns[a].anTfExtra[f];k.parentNode.removeChild(k)}}h=Z(d);f=0;for(e=d.aoData.length;f<e;f++){i=h[f*d.aoColumns.length+a*1];d.aoData[f]._anHidden[a]=i;i.parentNode.removeChild(i)}d.aoColumns[a].bVisible=false}f=0;for(e=d.aoOpenRows.length;f<e;f++)d.aoOpenRows[f].nTr.colSpan=S(d);if(typeof c=="undefined"||c){X(d);C(d)}na(d)}};this.fnPageChange=function(a,b){var c=A(this[n.iApiIndex]);ea(c,a);E(c);if(typeof b=="undefined"||b)C(c)};this.fnDestroy=
+function(){var a=A(this[n.iApiIndex]),b=a.nTableWrapper.parentNode,c=a.nTBody,d,f;a.bDestroying=true;j(a.nTableWrapper).find("*").andSelf().unbind(".DT");d=0;for(f=a.aoColumns.length;d<f;d++)a.aoColumns[d].bVisible===false&&this.fnSetColumnVis(d,true);j("tbody>tr>td."+a.oClasses.sRowEmpty,a.nTable).parent().remove();if(a.nTable!=a.nTHead.parentNode){j(">thead",a.nTable).remove();a.nTable.appendChild(a.nTHead)}if(a.nTFoot&&a.nTable!=a.nTFoot.parentNode){j(">tfoot",a.nTable).remove();a.nTable.appendChild(a.nTFoot)}a.nTable.parentNode.removeChild(a.nTable);
+j(a.nTableWrapper).remove();a.aaSorting=[];a.aaSortingFixed=[];T(a);j(R(a)).removeClass(a.asStripClasses.join(" "));if(a.bJUI){j("th",a.nTHead).removeClass([n.oStdClasses.sSortable,n.oJUIClasses.sSortableAsc,n.oJUIClasses.sSortableDesc,n.oJUIClasses.sSortableNone].join(" "));j("th span",a.nTHead).remove()}else j("th",a.nTHead).removeClass([n.oStdClasses.sSortable,n.oStdClasses.sSortableAsc,n.oStdClasses.sSortableDesc,n.oStdClasses.sSortableNone].join(" "));b.appendChild(a.nTable);d=0;for(f=a.aoData.length;d<
+f;d++)c.appendChild(a.aoData[d].nTr);a.nTable.style.width=v(a.sDestroyWidth);j(">tr:even",c).addClass(a.asDestoryStrips[0]);j(">tr:odd",c).addClass(a.asDestoryStrips[1]);d=0;for(f=D.length;d<f;d++)D[d]==a&&D.splice(d,1)};this.fnAdjustColumnSizing=function(a){var b=A(this[n.iApiIndex]);X(b);if(typeof a=="undefined"||a)this.fnDraw(false);else if(b.oScroll.sX!==""||b.oScroll.sY!=="")this.oApi._fnScrollDraw(b)};for(var pa in n.oApi)if(pa)this[pa]=r(pa);this.oApi._fnExternApiFunc=r;this.oApi._fnInitalise=
+s;this.oApi._fnLanguageProcess=y;this.oApi._fnAddColumn=F;this.oApi._fnColumnOptions=x;this.oApi._fnAddData=u;this.oApi._fnGatherData=z;this.oApi._fnDrawHead=U;this.oApi._fnDraw=C;this.oApi._fnReDraw=W;this.oApi._fnAjaxUpdate=ta;this.oApi._fnAjaxUpdateDraw=ua;this.oApi._fnAddOptionsHtml=sa;this.oApi._fnFeatureHtmlTable=za;this.oApi._fnScrollDraw=Ca;this.oApi._fnAjustColumnSizing=X;this.oApi._fnFeatureHtmlFilter=xa;this.oApi._fnFilterComplete=P;this.oApi._fnFilterCustom=Fa;this.oApi._fnFilterColumn=
+Ea;this.oApi._fnFilter=Da;this.oApi._fnBuildSearchArray=ha;this.oApi._fnBuildSearchRow=ka;this.oApi._fnFilterCreateSearch=ia;this.oApi._fnDataToSearch=ja;this.oApi._fnSort=O;this.oApi._fnSortAttachListener=ba;this.oApi._fnSortingClasses=T;this.oApi._fnFeatureHtmlPaginate=Ba;this.oApi._fnPageChange=ea;this.oApi._fnFeatureHtmlInfo=Aa;this.oApi._fnUpdateInfo=Ga;this.oApi._fnFeatureHtmlLength=wa;this.oApi._fnFeatureHtmlProcessing=ya;this.oApi._fnProcessingDisplay=K;this.oApi._fnVisibleToColumnIndex=ga;
+this.oApi._fnColumnIndexToVisible=M;this.oApi._fnNodeToDataIndex=Q;this.oApi._fnVisbleColumns=S;this.oApi._fnCalculateEnd=E;this.oApi._fnConvertToWidth=Ha;this.oApi._fnCalculateColumnWidths=$;this.oApi._fnScrollingWidthAdjust=Ja;this.oApi._fnGetWidestNode=Ia;this.oApi._fnGetMaxLenString=Ka;this.oApi._fnStringToCss=v;this.oApi._fnArrayCmp=Oa;this.oApi._fnDetectType=aa;this.oApi._fnSettingsFromNode=A;this.oApi._fnGetDataMaster=V;this.oApi._fnGetTrNodes=R;this.oApi._fnGetTdNodes=Z;this.oApi._fnEscapeRegex=
+la;this.oApi._fnDeleteIndex=ma;this.oApi._fnReOrderIndex=va;this.oApi._fnColumnOrdering=ca;this.oApi._fnLog=H;this.oApi._fnClearTable=da;this.oApi._fnSaveState=na;this.oApi._fnLoadState=Ma;this.oApi._fnCreateCookie=La;this.oApi._fnReadCookie=oa;this.oApi._fnGetUniqueThs=fa;this.oApi._fnScrollBarWidth=Na;this.oApi._fnApplyToChildren=L;this.oApi._fnMap=o;var qa=this;return this.each(function(){var a=0,b,c,d,f;a=0;for(b=D.length;a<b;a++){if(D[a].nTable==this)if(typeof g=="undefined"||typeof g.bRetrieve!=
+"undefined"&&g.bRetrieve===true)return D[a].oInstance;else if(typeof g.bDestroy!="undefined"&&g.bDestroy===true){D[a].oInstance.fnDestroy();break}else{H(D[a],0,"Cannot reinitialise DataTable.\n\nTo retrieve the DataTables object for this table, please pass either no arguments to the dataTable() function, or set bRetrieve to true. Alternatively, to destory the old table and create a new one, set bDestroy to true (note that a lot of changes to the configuration can be made through the API which is usually much faster).");
+return}if(D[a].sTableId!==""&&D[a].sTableId==this.getAttribute("id")){D.splice(a,1);break}}var e=new m;D.push(e);var i=false,h=false;a=this.getAttribute("id");if(a!==null){e.sTableId=a;e.sInstance=a}else e.sInstance=n._oExternConfig.iNextUnique++;if(this.nodeName.toLowerCase()!="table")H(e,0,"Attempted to initialise DataTables on a node which is not a table: "+this.nodeName);else{e.nTable=this;e.oInstance=qa.length==1?qa:j(this).dataTable();e.oApi=qa.oApi;e.sDestroyWidth=j(this).width();if(typeof g!=
+"undefined"&&g!==null){e.oInit=g;o(e.oFeatures,g,"bPaginate");o(e.oFeatures,g,"bLengthChange");o(e.oFeatures,g,"bFilter");o(e.oFeatures,g,"bSort");o(e.oFeatures,g,"bInfo");o(e.oFeatures,g,"bProcessing");o(e.oFeatures,g,"bAutoWidth");o(e.oFeatures,g,"bSortClasses");o(e.oFeatures,g,"bServerSide");o(e.oScroll,g,"sScrollX","sX");o(e.oScroll,g,"sScrollXInner","sXInner");o(e.oScroll,g,"sScrollY","sY");o(e.oScroll,g,"bScrollCollapse","bCollapse");o(e.oScroll,g,"bScrollInfinite","bInfinite");o(e.oScroll,
+g,"iScrollLoadGap","iLoadGap");o(e.oScroll,g,"bScrollAutoCss","bAutoCss");o(e,g,"asStripClasses");o(e,g,"fnRowCallback");o(e,g,"fnHeaderCallback");o(e,g,"fnFooterCallback");o(e,g,"fnCookieCallback");o(e,g,"fnInitComplete");o(e,g,"fnServerData");o(e,g,"fnFormatNumber");o(e,g,"aaSorting");o(e,g,"aaSortingFixed");o(e,g,"aLengthMenu");o(e,g,"sPaginationType");o(e,g,"sAjaxSource");o(e,g,"iCookieDuration");o(e,g,"sCookiePrefix");o(e,g,"sDom");o(e,g,"oSearch","oPreviousSearch");o(e,g,"aoSearchCols","aoPreSearchCols");
+o(e,g,"iDisplayLength","_iDisplayLength");o(e,g,"bJQueryUI","bJUI");o(e.oLanguage,g,"fnInfoCallback");typeof g.fnDrawCallback=="function"&&e.aoDrawCallback.push({fn:g.fnDrawCallback,sName:"user"});typeof g.fnStateSaveCallback=="function"&&e.aoStateSave.push({fn:g.fnStateSaveCallback,sName:"user"});typeof g.fnStateLoadCallback=="function"&&e.aoStateLoad.push({fn:g.fnStateLoadCallback,sName:"user"});e.oFeatures.bServerSide&&e.oFeatures.bSort&&e.oFeatures.bSortClasses&&e.aoDrawCallback.push({fn:T,sName:"server_side_sort_classes"});
+if(typeof g.bJQueryUI!="undefined"&&g.bJQueryUI){e.oClasses=n.oJUIClasses;if(typeof g.sDom=="undefined")e.sDom='<"H"lfr>t<"F"ip>'}if(e.oScroll.sX!==""||e.oScroll.sY!=="")e.oScroll.iBarWidth=Na();if(typeof g.iDisplayStart!="undefined"&&typeof e.iInitDisplayStart=="undefined"){e.iInitDisplayStart=g.iDisplayStart;e._iDisplayStart=g.iDisplayStart}if(typeof g.bStateSave!="undefined"){e.oFeatures.bStateSave=g.bStateSave;Ma(e,g);e.aoDrawCallback.push({fn:na,sName:"state_save"})}if(typeof g.aaData!="undefined")h=
+true;if(typeof g!="undefined"&&typeof g.aoData!="undefined")g.aoColumns=g.aoData;if(typeof g.oLanguage!="undefined")if(typeof g.oLanguage.sUrl!="undefined"&&g.oLanguage.sUrl!==""){e.oLanguage.sUrl=g.oLanguage.sUrl;j.getJSON(e.oLanguage.sUrl,null,function(q){y(e,q,true)});i=true}else y(e,g.oLanguage,false)}else g={};if(typeof g.asStripClasses=="undefined"){e.asStripClasses.push(e.oClasses.sStripOdd);e.asStripClasses.push(e.oClasses.sStripEven)}c=false;d=j(">tbody>tr",this);a=0;for(b=e.asStripClasses.length;a<
+b;a++)if(d.filter(":lt(2)").hasClass(e.asStripClasses[a])){c=true;break}if(c){e.asDestoryStrips=["",""];if(j(d[0]).hasClass(e.oClasses.sStripOdd))e.asDestoryStrips[0]+=e.oClasses.sStripOdd+" ";if(j(d[0]).hasClass(e.oClasses.sStripEven))e.asDestoryStrips[0]+=e.oClasses.sStripEven;if(j(d[1]).hasClass(e.oClasses.sStripOdd))e.asDestoryStrips[1]+=e.oClasses.sStripOdd+" ";if(j(d[1]).hasClass(e.oClasses.sStripEven))e.asDestoryStrips[1]+=e.oClasses.sStripEven;d.removeClass(e.asStripClasses.join(" "))}a=this.getElementsByTagName("thead");
+c=a.length===0?[]:fa(a[0]);var k;if(typeof g.aoColumns=="undefined"){k=[];a=0;for(b=c.length;a<b;a++)k.push(null)}else k=g.aoColumns;a=0;for(b=k.length;a<b;a++){if(typeof g.saved_aoColumns!="undefined"&&g.saved_aoColumns.length==b){if(k[a]===null)k[a]={};k[a].bVisible=g.saved_aoColumns[a].bVisible}F(e,c?c[a]:null)}if(typeof g.aoColumnDefs!="undefined")for(a=g.aoColumnDefs.length-1;a>=0;a--){var l=g.aoColumnDefs[a].aTargets;j.isArray(l)||H(e,1,"aTargets must be an array of targets, not a "+typeof l);
+c=0;for(d=l.length;c<d;c++)if(typeof l[c]=="number"&&l[c]>=0){for(;e.aoColumns.length<=l[c];)F(e);x(e,l[c],g.aoColumnDefs[a])}else if(typeof l[c]=="number"&&l[c]<0)x(e,e.aoColumns.length+l[c],g.aoColumnDefs[a]);else if(typeof l[c]=="string"){b=0;for(f=e.aoColumns.length;b<f;b++)if(l[c]=="_all"||e.aoColumns[b].nTh.className.indexOf(l[c])!=-1)x(e,b,g.aoColumnDefs[a])}}if(typeof k!="undefined"){a=0;for(b=k.length;a<b;a++)x(e,a,k[a])}a=0;for(b=e.aaSorting.length;a<b;a++){if(e.aaSorting[a][0]>=e.aoColumns.length)e.aaSorting[a][0]=
+0;k=e.aoColumns[e.aaSorting[a][0]];if(typeof e.aaSorting[a][2]=="undefined")e.aaSorting[a][2]=0;if(typeof g.aaSorting=="undefined"&&typeof e.saved_aaSorting=="undefined")e.aaSorting[a][1]=k.asSorting[0];c=0;for(d=k.asSorting.length;c<d;c++)if(e.aaSorting[a][1]==k.asSorting[c]){e.aaSorting[a][2]=c;break}}T(e);this.getElementsByTagName("thead").length===0&&this.appendChild(p.createElement("thead"));this.getElementsByTagName("tbody").length===0&&this.appendChild(p.createElement("tbody"));e.nTHead=this.getElementsByTagName("thead")[0];
+e.nTBody=this.getElementsByTagName("tbody")[0];if(this.getElementsByTagName("tfoot").length>0)e.nTFoot=this.getElementsByTagName("tfoot")[0];if(h)for(a=0;a<g.aaData.length;a++)u(e,g.aaData[a]);else z(e);e.aiDisplay=e.aiDisplayMaster.slice();e.bInitialised=true;i===false&&s(e)}})}})(jQuery,window,document);
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/loadbar.js
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/loadbar.js	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/loadbar.js	(revision 10886)
@@ -0,0 +1,23 @@
+
+var loadbar_interval = null;
+$(document).ready(
+    function update() {
+	$.getJSON(
+	    $("#status_url").attr("value"),
+	    {},
+	    function(data) {
+		$('input[name="reload"]').remove();
+		clearInterval(loadbar_interval);
+		$("#loadbar").html(data.html);
+		if (data.reload == true) {
+		    if ($('input[name="download"]').attr("value") == null) {
+			location.reload();
+		    }
+		}
+		if (data.interval != 0) {
+		    loadbar_interval = setInterval(update, data.interval);
+		}
+	    }
+	)
+    }
+)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/mobile.css
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/mobile.css	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/mobile.css	(revision 10886)
@@ -0,0 +1,7 @@
+.container {
+  width:900px;
+}
+
+.student-info-bar{
+  width: 900px;
+}
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/page_not_reloader.js
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/page_not_reloader.js	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/page_not_reloader.js	(revision 10886)
@@ -0,0 +1,8 @@
+$(document).ready(function() {
+    if(window.top==window) {
+	// you're not in a frame so you reload the site
+	window.clearTimeout('location.reload()'); //reloads after 1.5 seconds
+    } else {
+	//you're inside a frame, so you stop reloading
+    }
+});
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/page_reloader.js
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/page_reloader.js	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/page_reloader.js	(revision 10886)
@@ -0,0 +1,8 @@
+$(document).ready(function() {
+    if(window.top==window) {
+	// you're not in a frame so you reload the site
+	window.setTimeout('location.reload()', 1500); //reloads after 1.5 seconds
+    } else {
+	//you're inside a frame, so you stop reloading
+    }
+});
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/recaptcha_white.js
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/recaptcha_white.js	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/recaptcha_white.js	(revision 10886)
@@ -0,0 +1,3 @@
+var RecaptchaOptions = {
+    theme : 'white'
+ };
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/tabs.js
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/tabs.js	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/tabs.js	(revision 10886)
@@ -0,0 +1,4 @@
+/* See http://docs.jquery.com/UI/Tabs */
+$(document).ready(function() {
+    $("#tabs").tabs();
+  });
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/toggleall.js
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/toggleall.js	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/toggleall.js	(revision 10886)
@@ -0,0 +1,5 @@
+function toggle(source, name) {
+  checkboxes = document.getElementsByName(name);
+  for(var i in checkboxes)
+    checkboxes[i].checked = source.checked;
+}
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/waeup-base.css
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/waeup-base.css	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/waeup-base.css	(revision 10886)
@@ -0,0 +1,264 @@
+/* This is the base stylesheet for KOFA. It defines base styles
+additionally or modifying Bootstrap styles. For themes, please create
+a stylesheet overriding values set here in a file named
+'waeuptheme-<SOME-NAME>.css' and include a utility and a resource in theming.py 
+and resources.py respectively. */
+
+/* bootstrap.min customizations */
+
+body {
+  padding-top: 56px;
+  background-color: #EEEEEE;
+}
+
+.container-fluid>.sidebar {
+  width: 165px;
+}
+
+.container-fluid > .content {
+  margin-left: 180px;
+}
+
+.well {
+  border: 0px;
+  background-color: #FFFFFF;
+  padding-bottom: 12px;
+  padding-left: 12px;
+  padding-right: 12px;
+  padding-top: 12px;
+  box-shadow: none;
+}
+
+.content {
+  background-color: #FFFFFF;
+  margin-left: -20px;
+  margin-right: -20px;
+  padding-bottom: 16px;
+  padding-left: 20px;
+  padding-right: 20px;
+  padding-top: 16px;
+  border-top-left-radius: 3px;
+  border-top-right-radius: 3px;
+}
+
+.container {
+  width: 800px;
+}
+
+h2 {
+  margin-bottom: 12px;
+}
+
+.breadcrumb {
+  margin-left: 20px;
+  margin-right: 20px;
+  margin-bottom: 14px;
+  padding-left: 20px;
+  padding-right: 20px;
+  width: 940px;
+  background-color: #FFFFFF;
+  background-image: None;
+  border: 0px;
+  border-top-left-radius: 3px;
+  border-top-right-radius: 3px;
+  border-bottom-left-radius: 3px;
+  border-bottom-right-radius: 3px;
+}
+
+
+select {
+  width: 310px;
+  padding: 4px;
+}
+
+h1 {
+  font-size: 22px;
+}
+h2 {
+  font-size: 18px;
+}
+h3 {
+  font-size: 16px;
+}
+h4 {
+  font-size: 14px;
+}
+h5 {
+  font-size: 13px;
+  line-height: 18px;
+  padding-bottom: 9px;
+  padding-top: 9px;
+}
+h6 {
+  font-size: 12px;
+}
+
+.btn {
+  padding-bottom: 4px;
+  padding-left: 8px;
+  padding-right: 8px;
+  padding-top: 4px;
+  text-align: center;
+}
+
+.btn.small {
+    font-size: 11px;
+    padding: 3px 0px;
+    width: 60px;
+}
+
+.breadcrumb .divider {
+  padding: 0 3px;
+  color: #bfbfbf;
+}
+
+textarea {
+  font-family: Courier, "Courier New", monospace;
+}
+
+ /* IE doesn't like the footer tag*/
+.footer {
+  padding-top: 17px;
+  border-top: 1px solid #eee;
+  margin-top: 50px;
+}
+
+ /* dataTables stuff */
+
+.dataTables_wrapper {
+  min-height: 50px;
+  margin-bottom: 50px;
+}
+
+.dataTables_filter {
+  margin-bottom: 20px;
+}
+
+/* Background temporarily disabled */
+
+.sorting_asc, .sorting_desc, .sorting {
+  background: None;
+}
+
+ /* KOFA stuff */
+
+div.actionbar {
+  margin-top: -24px;
+  margin-left: -15px;
+  margin-bottom: 6px;
+}
+
+a.actionbar {
+  margin-bottom: 6px;
+}
+
+.error {
+  color: red;
+}
+
+.form-table {
+  width: 100%;
+}
+
+.input-table {
+  width: 50%;
+}
+
+.form-table td, .input-table td {
+  padding: 5px 0px 5px 4px;
+  border-top: none;
+}
+
+input, textarea, select, .uneditable-input {
+  width: 250px;
+}
+
+.fieldname {
+  width: 25%;
+}
+
+.workflow {
+  border:1px solid #1fdf00;
+  background-color:#bbffb6;
+  font-family: courier;
+  font-size: 100%;
+  padding: 4px;
+  margin-bottom: 8px;
+  line-height: 16px;
+}
+
+.student-info-bar{
+  padding: 7px 14px;
+  margin-left: -20px;
+  margin-right: -20px;
+  margin-bottom: 14px;
+  padding-left: 20px;
+  padding-right: 20px;
+  width: 800px;
+  background-color: #FFFFFF;
+  background-image: None;
+  border: 0px;
+  border-top-left-radius: 3px;
+  border-top-right-radius: 3px;
+  border-bottom-left-radius: 3px;
+  border-bottom-right-radius: 3px;
+}
+
+.student-info-bar li {
+  display: inline;
+  text-shadow: 0 1px 0 #ffffff;
+}
+.student-info-bar .divider {
+  padding: 0 0px;
+  color: #bfbfbf;
+}
+.student-info-bar .active a {
+  color: #404040;
+}
+.wfstatus {
+  float: right;
+  line-height: 18px;
+}
+
+.language {
+  color: #BFBFBF;
+  position:relative;
+  top:10px;
+  right:-60px;
+}
+
+.language a {
+  color: #BFBFBF;
+}
+
+.language a:hover {
+  color: #ffffff;
+}
+
+.hint {
+  padding: 0px 0px 5px 0px;
+  color: #bfbfbf;
+}
+
+.separator {
+  font-size: 16px;
+  color: #404040;
+  font-weight: bold;
+  line-height: 36px;
+}
+
+table .ordered-selection-field select {
+  width: 250px;
+}
+
+.alert-message.error ul {
+    margin: 0 0 0px 25px
+}
+
+.alert-message.error, .alert-message.error error, .alert-message.error li {
+    color: white;
+}
+
+#alert-error .error{
+    color: white;
+}
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/warning.js
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/warning.js	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/static/warning.js	(revision 10886)
@@ -0,0 +1,7 @@
+function confirmPost(question) {
+    var agree=confirm(question);
+    if (agree)
+    return true ;
+    else
+    return false ;
+};
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/actionbutton.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/actionbutton.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/actionbutton.pt	(revision 10886)
@@ -0,0 +1,7 @@
+<a class="btn actionbar" 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/tags/0.2/src/waeup/kofa/browser/templates/addaction.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/addaction.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/addaction.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/templates/administrationpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/administrationpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/administrationpage.pt	(revision 10886)
@@ -0,0 +1,5 @@
+<p>
+  You can manage the following tasks:
+</p>
+
+<tal:admintasks tal:content="structure provider:admintasks" />
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/admintask.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/admintask.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/admintask.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/templates/breadcrumbs.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/breadcrumbs.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/breadcrumbs.pt	(revision 10886)
@@ -0,0 +1,8 @@
+<span tal:repeat="item viewlet/getEntries">
+  <li>
+    <a href="#" tal:attributes="href item/url">
+      <span tal:content="item/title">Entry</span>
+    </a>
+    <span tal:condition="not: repeat/item/end" class="divider">/</span>
+  </li>
+</span>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/certificatecoursepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/certificatecoursepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/certificatecoursepage.pt	(revision 10886)
@@ -0,0 +1,37 @@
+<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 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>
+      <td i18n:translate="">Required course:</td>
+      <td tal:content="context/mandatory">REQUIRED</td>
+    </tr>
+  </tbody>
+</table>
+
+
+
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/certificatemanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/certificatemanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/certificatemanagepage.pt	(revision 10886)
@@ -0,0 +1,146 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data" i18n:domain="waeup.kofa">
+
+  <ul class="tabs" data-tabs="tabs">
+    <li tal:attributes="class view/tab1"><a href="#tab-1" i18n:translate="">Settings</a></li>
+    <li tal:attributes="class view/tab2"><a href="#tab-2" i18n:translate="">Certificate Courses</a></li>
+    <li tal:attributes="class view/tab3"><a href="#tab-3" i18n:translate="">Local Roles</a></li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+    <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="tab-2" tal:attributes="class view/tab2">
+      <h3>
+        <span tal:content="context/__name__">Code</span>
+        <span i18n:domain="waeup.kofa" i18n:translate="">Courses</span>
+      </h3>
+      <table class="display 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="">Required</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="cc context/values" class="gradeC">
+             <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>
+              <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="tab-3" tal:attributes="class view/tab3"> <br />
+      <table class="display 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" class="gradeC"> <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">
+              <option tal:repeat="user view/getUsers"
+                tal:attributes="value user/name">
+              <span tal:replace="user/val/title">USERTITLE
+              </span>
+              </option>
+            </select> </td> <td>
+            <select 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> </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/tags/0.2/src/waeup/kofa/browser/templates/certificatepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/certificatepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/certificatepage.pt	(revision 10886)
@@ -0,0 +1,68 @@
+<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="display 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="">Required</th>
+  </tr>
+</thead>
+<tbody>
+  <tr tal:repeat="cc context/values" class="gradeC">
+     <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>
+      <span tal:content="cc/mandatory">
+	   REQUIRED
+      </span>
+    </td>
+  </tr>
+</tbody>
+</table>
+
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/changepw.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/changepw.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/changepw.pt	(revision 10886)
@@ -0,0 +1,33 @@
+<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" />
+      <tr tal:condition="view/captcha_code">
+        <td i18n:translate="solve_captcha">
+          Please solve the captcha<br />to prevent misuse of this service:
+        </td>
+        <td>
+          <div tal:content="structure view/captcha_code"></div>
+        </td>
+      </tr>
+
+    </tbody>
+  </table>
+
+  <p 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>
+  <br />
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/configurationmanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/configurationmanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/configurationmanagepage.pt	(revision 10886)
@@ -0,0 +1,50 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+  enctype="multipart/form-data" i18n:domain="waeup.kofa">
+    <ul class="tabs" data-tabs="tabs">
+      <li tal:attributes="class view/tab1">
+      <a href="#tab-1" i18n:translate="">Site Settings</a>
+      </li>
+      <li tal:attributes="class view/tab2">
+      <a href="#tab-2" i18n:translate="">Session Configurations</a>
+      </li>
+    </ul>
+    <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+      <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="tab-2" tal:attributes="class view/tab2"> <br />
+      <table class="display dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">Name
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getSessionConfigurations" class="gradeC"> <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>
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/contactform.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/contactform.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/contactform.pt	(revision 10886)
@@ -0,0 +1,32 @@
+<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>
+    <p i18n:translate="">
+      Please solve the captcha to prevent misuse of this service.
+    </p>
+  </tal:captcha>
+
+    <div tal:condition="view/availableActions">
+      <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+    </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/coursemanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/coursemanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/coursemanagepage.pt	(revision 10886)
@@ -0,0 +1,83 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data" i18n:domain="waeup.kofa">
+
+  <ul class="tabs" data-tabs="tabs">
+    <li tal:attributes="class view/tab1"><a href="#tab-1" i18n:translate="">Settings</a></li>
+    <li tal:attributes="class view/tab2"><a href="#tab-3" i18n:translate="">Local Roles</a></li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+    <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="tab-3" tal:attributes="class view/tab2"> <br />
+      <table class="display 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" class="gradeC"> <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 />
+      <table class="form-table">
+        <tr> <td>
+            <select id="user" name="user">
+              <option tal:repeat="user view/getUsers"
+                tal:attributes="value user/name">
+              <span tal:replace="user/val/title">USERTITLE
+              </span>
+              </option>
+            </select> </td> <td>
+            <select 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> </td> <td>
+            <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> </td>
+        </tr>
+      </table>
+    </div>
+  </div>
+</form>
+
+
+
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterexportpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterexportpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterexportpage.pt	(revision 10886)
@@ -0,0 +1,94 @@
+<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.
+</p>
+
+<br />
+
+<form  method="POST">
+  <label for="exporter">
+    Exporter (Data Type):
+  </label>
+  <div class="input">
+    <select name="exporter">
+      <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>
+  <br />
+  <div class="input">
+    <input i18n:translate="" type="submit" class="btn primary"
+      name="CREATE" tal:attributes="value view/export_button" />
+  </div>
+</form>
+
+<br />
+
+<p i18n:translate="">
+  Student data exports have to be further configured. You can create CSV files
+  on the <a href="@@exportconfig">student data export configuration page</a>.
+</p>
+
+<br /><br />
+
+<table i18n:domain="waeup.kofa">
+  <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 nowrap>
+        <span tal:replace="job/start_time">DATETIME</span>
+      </td>
+      <td>
+        <span tal:replace="job/status">STATUS</span>
+      </td>
+      <td nowrap>
+        <a href="" class="btn small" i18n:translate=""
+           tal:condition="job/show_refresh_button">
+          <img tal:attributes="src static/actionicon_reload.png" />
+          Reload
+        </a>
+        <form method="POST">
+          <a href="" class="btn primary small"
+                 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 small"
+                 name="DISCARD" value="Discard"
+                 tal:condition="job/show_discard_button" />
+        </form>
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterfinishedpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterfinishedpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterfinishedpage.pt	(revision 10886)
@@ -0,0 +1,22 @@
+<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="display 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" class="gradeC">
+      <td><a tal:attributes="href python: '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>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterimport1page.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterimport1page.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterimport1page.pt	(revision 10886)
@@ -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>
+    <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 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"
+      tal:attributes="value view/cancel_button" />
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterimport2page.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterimport2page.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterimport2page.pt	(revision 10886)
@@ -0,0 +1,62 @@
+<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>
+  <div>
+    <table tal:define="prev_table view/getPreviewTable;">
+      <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>
+  <div> <b i18n:translate="">Processor:</b>
+    <select name="importer">
+      <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" type="submit" name="cancel" tal:attributes="value view/cancel_button" />
+  <input class="btn danger" type="submit" name="back1" tal:attributes="value view/back_button" />
+  <input class="btn primary"
+    tal:attributes="disabled python: view.getWarnings() and 'disabled' or ''; value view/proceed_button"
+    type="submit" name="proceed" />
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterimport3page.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterimport3page.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterimport3page.pt	(revision 10886)
@@ -0,0 +1,76 @@
+<div i18n:domain="waeup.kofa" i18n:translate=""
+  class="alert-message 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;">
+      <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 class="span4" name="headerfield">
+              <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" type="submit" name="cancel"
+    tal:attributes="value view/cancel_button" />
+  <input class="btn" type="reset" name="reset"
+    tal:attributes="value view/reset_button" />
+  <input class="btn" type="submit" name="update"
+    tal:attributes="value view/update_button" />
+  <br /><br />
+  <input class="btn danger" type="submit" name="back2"
+    tal:attributes="value view/back_button" />
+  <input class="btn primary"
+    tal:attributes="disabled python: view.getWarnings() and 'disabled' or ''; value view/proceed_button"
+    type="submit" name="proceed"/>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterimport4page.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterimport4page.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterimport4page.pt	(revision 10886)
@@ -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 primary" type="submit" name="finish"
+    tal:attributes="value view/back_button" />
+</form>
+
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterlogspage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterlogspage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterlogspage.pt	(revision 10886)
@@ -0,0 +1,41 @@
+<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 condensed-table">
+    <thead>
+      <tr>
+        <th>
+        </th>
+        <th i18n:translate="">File
+        </th>
+        <th i18n:translate="">Date
+        </th>
+        <th i18n:translate="">Size
+        </th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="file files"
+          tal:attributes="class python: repeat['file'].odd() and 'odd' or 'even'">
+        <td class="text-center">
+          <form method="POST" action="@@show">
+            <input i18n:translate="" type="submit" class="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>
+        <td tal:content="file/getSize">12 bytes</td>
+      </tr>
+    </tbody>
+  </table>
+  <form method="POST">
+    <button class="btn primary">
+      <span i18n:translate="">Reload page</span>
+    </button>
+    <input i18n:translate="" type="submit" class="btn" name="back"
+        tal:attributes="value view/back_button"/>
+  </form>
+</div>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterlogsshowfilepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterlogsshowfilepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterlogsshowfilepage.pt	(revision 10886)
@@ -0,0 +1,16 @@
+<form method="post" class="form-horizontal">
+  <input type="text" name="query"
+	 tal:attributes="value view/query; placeholder view/placeholder" />
+  <input class="btn primary" type="submit" name="search"
+	 tal:attributes="value view/search_button" />
+  <input class="btn" type="submit" name="back"
+	 tal:attributes="value view/back_button" />
+  <input type="hidden" name="logname"
+	 tal:attributes="value view/filename" />
+</form>
+
+<div tal:condition="view/result|nothing">
+  <pre tal:content="structure view/result">
+    GREPRESULT
+  </pre>
+</div>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacentermanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacentermanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacentermanagepage.pt	(revision 10886)
@@ -0,0 +1,24 @@
+<form  i18n:domain="waeup.kofa" method="POST">
+  <div>
+    <span  i18n:translate="">Storage path:</span>
+    <input type="text" name="newpath"
+     tal:attributes="value context/storage" />
+    <br /><br />
+    <span  i18n:translate="">
+    Copy existing data to new storage?
+    </span>&nbsp;&nbsp;
+    <input type="checkbox" name="move" checked="checked" />
+    <br /><br />
+    <span  i18n:translate="">
+    Overwrite contents in new location?
+    </span>&nbsp;&nbsp;
+    <input type="checkbox" name="overwrite" checked="no" />
+    <br /><br />
+    <input class="btn primary" type="submit" name="save"
+        tal:attributes="value view/save_button" />
+    <input class="btn" type="reset" name="reset"
+        tal:attributes="value view/reset_button" />
+    <input class="btn" type="submit" name="cancel"
+        tal:attributes="value view/cancel_button" />
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenterpage.pt	(revision 10886)
@@ -0,0 +1,40 @@
+<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="display 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" class="gradeC">
+          <td>
+            <input type="checkbox" name="val_id"
+                         tal:attributes="value file/name" />
+          </td>
+          <td><a tal:attributes="href python: '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/tags/0.2/src/waeup/kofa/browser/templates/datacenteruploadpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenteruploadpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/datacenteruploadpage.pt	(revision 10886)
@@ -0,0 +1,117 @@
+<p i18n:translate="">
+    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.
+</p>
+
+<form method="post" enctype="multipart/form-data">
+<br />
+  <table class="form-table">
+    <tr>
+      <td i18n:translate="">
+        File:
+      </td>
+      <td>
+        <input type="file" size=47 name="uploadfile:file" />
+      </td>
+    </tr>
+    <tr>
+      <td i18n:translate="">
+        Processor:
+      </td>
+      <td>
+        <select name="importer">
+          <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">
+          <option value='Create'>Create</option>
+          <option value='Update'>Update</option>
+          <option value='Remove'>Remove</option>
+        </select>
+      </td>
+    </tr>
+    <tr>
+      <td>
+        <input class="btn primary" type="submit" name="SUBMIT"
+            tal:attributes="value view/upload_button" />
+        <input class="btn" type="submit" name="CANCEL"
+            tal:attributes="value view/cancel_button"/>
+      </td>
+      <td>
+      </td>
+    </tr>
+  </table>
+</form>
+
+<p i18n:translate="">
+    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.
+</p>
+
+<br /><br />
+
+<h2 i18n:translate="">Available Processors (Importers)</h2>
+
+<table i18n:domain="waeup.kofa">
+  <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>
+      <th i18n:translate="">CSV Skeleton File</th>
+    </tr>
+  </thead>
+  <tr tal:repeat="importer view/getImporters">
+    <td tal:content="importer/title"></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>
+
+    <td>
+      <a i18n:translate=""
+         tal:attributes="href python: 'skeleton?name=' + importer['name']">Download</a>
+    </td>
+  </tr>
+</table>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/default_waeup_display_form.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/default_waeup_display_form.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/default_waeup_display_form.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/templates/default_waeup_edit_form.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/default_waeup_edit_form.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/default_waeup_edit_form.pt	(revision 10886)
@@ -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" />
+      <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/tags/0.2/src/waeup/kofa/browser/templates/departmentmanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/departmentmanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/departmentmanagepage.pt	(revision 10886)
@@ -0,0 +1,146 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+  i18n:domain="waeup.kofa" enctype="multipart/form-data">
+  <ul class="tabs" data-tabs="tabs">
+    <li tal:attributes="class view/tab1">
+    <a href="#tab-1">
+      <span i18n:translate="">Settings
+      </span></a>
+    </li>
+    <li tal:attributes="class view/tab2">
+    <a href="#tab-2" i18n:translate="">Courses</a>
+    </li>
+    <li tal:attributes="class view/tab3">
+    <a href="#tab-3" i18n:translate="">Certificates</a>
+    </li>
+    <li tal:attributes="class view/tab4">
+    <a href="#tab-4" i18n:translate="">Local Roles</a>
+    </li>
+  </ul>
+  <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+      <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="tab-2" tal:attributes="class view/tab2"> <br />
+      <table class="display 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" class="gradeC"> <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="tab-3" tal:attributes="class view/tab3"> <br />
+      <table class="display 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" class="gradeC"> <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="tab-4" tal:attributes="class view/tab4"> <br />
+      <table class="display 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" class="gradeC">
+            <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 />
+      <table class="form-table">
+        <tr> <td>
+            <select id="user" name="user">
+              <option tal:attributes="value user/name" tal:repeat="user view/getUsers">
+              <span tal:replace="user/val/title">USERTITLE
+              </span>
+              </option>
+            </select> </td> <td>
+            <select id="local_role" name="local_role">
+              <option tal:attributes="value localrole/name"
+                tal:repeat="localrole view/getLocalRoles">
+              <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.tabfouractions2"
+                  tal:replace="structure action/render"/>
+              </span>
+            </div> </td>
+        </tr>
+      </table>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/departmentpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/departmentpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/departmentpage.pt	(revision 10886)
@@ -0,0 +1,56 @@
+<ul i18n:domain="waeup.kofa" class="tabs" data-tabs="tabs">
+  <li class="active"><a href="#tab-1">
+   <span i18n:translate="">Courses</span></a>
+  </li>
+  <li><a href="#tab-2">
+    <span i18n:translate="">Certificates</span></a>
+  </li>
+</ul>
+<div  i18n:domain="waeup.kofa" class="tab-content">
+    <div id="tab-1" class="active">
+    <br />
+    <table class="display dataTable">
+      <thead>
+        <tr>
+          <th i18n:translate="">Code</th>
+          <th i18n:translate="">Title</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="entry view/getCourses" class="gradeC">
+          <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="tab-2">
+    <br />
+    <table class="display dataTable">
+      <thead>
+        <tr>
+          <th i18n:translate="">Code</th>
+          <th i18n:translate="">Title</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="entry view/getCertificates" class="gradeC">
+          <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/tags/0.2/src/waeup/kofa/browser/templates/exception.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/exception.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/exception.pt	(revision 10886)
@@ -0,0 +1,9 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <title>System Error</title>
+  </head>
+  <body>
+    <h1>A system error occured</h1>
+    <div>We apologize for any inconveniences</div>
+  </body>
+</html>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/exportxmlaction.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/exportxmlaction.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/exportxmlaction.pt	(revision 10886)
@@ -0,0 +1,8 @@
+<span class="yui-button" noclass="actionbarlink">
+  <span class="first-child">
+    <a href="" id="linkbutton1"
+       tal:attributes="href python: view.url(context) + '/export.xml'">
+      <img alt="export as XML"
+	   tal:attributes="src static/actionicon_unpublish.png"/>
+  <span>Export as XML</span></a></span>
+</span>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/facultiescontainermanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/facultiescontainermanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/facultiescontainermanagepage.pt	(revision 10886)
@@ -0,0 +1,38 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data">
+
+  <table>
+    <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/tags/0.2/src/waeup/kofa/browser/templates/facultymanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/facultymanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/facultymanagepage.pt	(revision 10886)
@@ -0,0 +1,120 @@
+<form action="." tal:attributes="action request/URL"
+  i18n:domain="waeup.kofa" method="POST" enctype="multipart/form-data">
+  <ul class="tabs" data-tabs="tabs">
+    <li tal:attributes="class view/tab1">
+    <a href="#tab-1">
+      <span i18n:translate="">Settings
+      </span></a>
+    </li>
+    <li tal:attributes="class view/tab2">
+    <a href="#tab-2">
+      <span tal:content="view/subunits">CONTENTS
+      </span> </a>
+    </li>
+    <li tal:attributes="class view/tab3">
+    <a href="#tab-3">
+      <span i18n:translate="">Local Roles
+      </span></a>
+    </li>
+  </ul>
+  <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+      <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="tab-2" tal:attributes="class view/tab2">
+      <h3 tal:content="view/subunits">DEPARTMENTS</h3>
+      <table>
+        <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="tab-3" tal:attributes="class view/tab3"> <br />
+      <table class="display 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" class="gradeC"> <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">
+              <option tal:repeat="user view/getUsers" tal:attributes="value user/name">
+              <span tal:replace="user/val/title">USERTITLE
+              </span>
+              </option>
+            </select> </td> <td>
+            <select 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> </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/tags/0.2/src/waeup/kofa/browser/templates/facultypage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/facultypage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/facultypage.pt	(revision 10886)
@@ -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 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 value/__name__">
+          <span tal:content="value/code">Name</span></a></td>
+      <td tal:content="value/longtitle">Title</td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/filedisplay.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/filedisplay.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/filedisplay.pt	(revision 10886)
@@ -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 viewlet/download_name"
+       tal:content="viewlet/download_name"
+       target="image">
+      LINK
+    </a>
+  </td>
+</tr>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/fileupload.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/fileupload.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/fileupload.pt	(revision 10886)
@@ -0,0 +1,32 @@
+<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 viewlet/download_name"
+         tal:content="viewlet/title"
+         target="image">
+        LINK
+      </a>
+    </tal:file>
+    <br />
+    <input type="file" tal:attributes="name viewlet/input_name"/>
+    <br /><br />
+    <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 viewlet/delete_button"
+           class="btn error" />
+    <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 primary" />
+    <span i18n:translate="">
+      Max. file size:
+    </span>
+    <span tal:replace="viewlet/max_upload_size">10 KB</span>
+    <br />
+  </td>
+</tr>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/findstudentspage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/findstudentspage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/findstudentspage.pt	(revision 10886)
@@ -0,0 +1,50 @@
+<form method="POST" i18n:domain="waeup.kofa">
+<br />
+  <input class="btn primary" type="submit" name="search"
+    tal:attributes="value view/search_button" />
+
+  <select name="searchtype">
+    <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>
+
+  <input type="text" name="searchterm" />
+
+  <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="display 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" class="gradeC">
+        <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>
+    <br />
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/importxmlaction.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/importxmlaction.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/importxmlaction.pt	(revision 10886)
@@ -0,0 +1,9 @@
+<span tal:condition="viewlet/can_import"
+      class="yui-button" noclass="actionbarlink">
+  <span class="first-child">
+    <a href="" id="linkbutton1"
+       tal:attributes="href python: view.url(context) + '/importxml'">
+      <img alt="import from XML"
+	   tal:attributes="src static/actionicon_submit.png"/>
+  <span>Import from XML</span></a></span>
+</span>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/importxmlpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/importxmlpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/importxmlpage.pt	(revision 10886)
@@ -0,0 +1,17 @@
+<div>
+  <h2>Import from XML source</h2>
+
+  <div>
+    Please specify an XML source file
+  </div>
+
+  <div>
+    <form method="post" enctype="multipart/form-data">
+      <input type="file" name="xmlfile:file" />
+      <div>
+	<input type="submit" name="SUBMIT" value="Upload" />
+	<input type="submit" name="CANCEL" value="Cancel" />
+      </div>
+    </form>
+  </div>
+</div>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/jobindex.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/jobindex.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/jobindex.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/templates/loginpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/loginpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/loginpage.pt	(revision 10886)
@@ -0,0 +1,51 @@
+<form i18n:domain="waeup.kofa" method="post">
+  <table id="login" class="form-table" summary="Table for entering login information">
+    <tbody>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          User Name or Id:
+        </td>
+        <td>
+          <input type="text" id="login" name="form.login" size="20" />
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">
+          Password:
+        </td>
+        <td>
+          <input type="password" id="password" name="form.password"  size="20" value="" />
+          &nbsp;&nbsp;
+          </td>
+      </tr>
+      <tr>
+        <td>&nbsp;</td>
+        <td>
+          <font  i18n:translate="" color="red"> Notice: User names, Ids and passwords are case sensitive.</font>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+  <input type="hidden" name="camefrom"   tal:attributes="value view/camefrom" />
+  <input class="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, application id or password? Then request a new password
+    <strong><a href="changepw"> here</a></strong>.
+  </p>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/logoutpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/logoutpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/logoutpage.pt	(revision 10886)
@@ -0,0 +1,2 @@
+<div>
+</div>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/messages.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/messages.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/messages.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/templates/myrolespage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/myrolespage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/myrolespage.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/templates/notfound.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/notfound.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/notfound.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/templates/plainactionbutton.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/plainactionbutton.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/plainactionbutton.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/templates/primarynavtab.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/primarynavtab.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/primarynavtab.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/templates/reportscontainercreate.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/reportscontainercreate.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/reportscontainercreate.pt	(revision 10886)
@@ -0,0 +1,33 @@
+<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="clearfix">
+    <label for="generator">Report Type:</label>
+    <div class="input">
+      <select name="generator">
+        <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 class="help-inline" i18n:translate="">
+        Type of report to create
+      </div>
+    </div>
+  </div>
+  <div class="input">
+    <input i18n:translate="" type="submit" class="btn primary"
+           name="START_GENERATOR" value="Configure" />
+    <a class="btn secondary" href=""
+       tal:attributes="href python:view.url(context)">Cancel</a>
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/reportscontainerpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/reportscontainerpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/reportscontainerpage.pt	(revision 10886)
@@ -0,0 +1,55 @@
+<table i18n:domain="waeup.kofa">
+  <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 small primary"
+                 name="DOWNLOAD" value="Download"
+                 tal:condition="python: entry[4]" />
+          <input type="submit" class="btn small secondary"
+                 name="DISCARD" value="Discard"
+                 tal:condition="python: entry[3]" />
+          <a href="" class="btn" i18n:translate=""
+             tal:condition="python: entry[5]">
+            <img tal:attributes="src static/actionicon_reload.png" />
+            Reload
+          </a>
+        </form>
+      </td>
+    </tr>
+  </tbody>
+</table>
+<form method="POST" i18n:domain="waeup.kofa">
+  <div class="span12">
+    <a href="create" class="btn primary" i18n:translate="">
+      Create new report
+    </a>
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/searchpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/searchpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/searchpage.pt	(revision 10886)
@@ -0,0 +1,41 @@
+<form method="post">
+  <input type="text" name="query"
+	 tal:attributes="value view/query" />
+    <input class="btn primary" type="submit" name="SUBMIT"
+      tal:attributes="value view/search_button"/>
+</form>
+<br />
+<span  i18n:domain="waeup.kofa" tal:condition="view/hitlist">
+    <h3 i18n:translate="">Search Results</h3>
+    <table class="display 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/tags/0.2/src/waeup/kofa/browser/templates/staffsitelayout.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/staffsitelayout.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/staffsitelayout.pt	(revision 10886)
@@ -0,0 +1,134 @@
+<!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>
+    <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="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>
+  <body>
+    <div class="topbar" data-scrollspy="scrollspy" >
+      <div class="topbar-inner">
+        <div class="container-fluid">
+          <a class="brand" href="#"
+              tal:attributes="href python: view.url(layout.site)"
+              tal:content="layout/getAppAcronym">ACRONYM</a>
+          <ul class="nav kofa-navbar-left">
+            <tal:primary_nav content="structure provider:primary_nav" />
+          </ul>
+          <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>
+          <span class="language"
+                tal:define="langs provider:languages"
+                tal:condition="langs">
+            <tal:languages
+                  tal:content="structure langs" /> |
+          </span>
+        </div>
+      </div>
+    </div>
+
+    <ul class="breadcrumb">
+      <tal:breadcrumbs
+            tal:condition="python: layout.isAuthenticated()"
+            tal:content="structure provider:breadcrumbs" />
+      <span
+        tal:condition="layout/getStudentName" class="wfstatus">
+          <span i18n:translate="">Student Id:</span>
+          <span tal:replace="python:context.student.student_id">
+            ID
+          </span>&nbsp;&nbsp;
+          <span i18n:translate="">State:</span>
+          <span tal:replace="python:context.student.translated_state">
+            STATE
+          </span>
+      </span>
+    </ul>
+
+    <div class="container-fluid">
+      <div class="sidebar">
+        <div class="well">
+          <div tal:condition="layout/getStudentName">
+            <h5 tal:content="layout/getStudentName">STUDENTNAME</h5>
+            <ul>
+              <tal:left content="structure provider:left_studentmanage" />
+            </ul>
+          </div>
+          <div tal:condition="layout/isAuthenticated">
+            <h5 tal:content="layout/getUserTitle">USERNAME</h5>
+            <ul>
+              <tal:left content="structure provider:left_manage" />
+            </ul>
+          </div>
+        </div>
+      </div>
+      <div class="span13 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-message error"
+          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>
+        <div class="pull-right">
+          <a i18n:translate="" href="#footer">Back to bottom</a>
+        </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>
+        <div id="footer" class="footer">
+          <div class="pull-right">
+            <a i18n:translate="" href="#">Back to top</a>
+          </div>
+          <div i18n:translate="">
+            Kofa - Student Management System
+          </div>
+          <div i18n:translate="">
+            Copyright &copy; WAeUP Group 2008-2013
+          </div>
+        </div>
+      </div>
+    </div>
+  </body>
+</html>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/studentsitelayout.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/studentsitelayout.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/studentsitelayout.pt	(revision 10886)
@@ -0,0 +1,119 @@
+<!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>
+  <body>
+    <div class="topbar" data-scrollspy="scrollspy" >
+      <div class="topbar-inner">
+        <div  class="container">
+          <a class="brand" href="#"
+              tal:attributes="href python: view.url(layout.site)"
+              tal:content="layout/getAppAcronym">ACRONYM</a>
+          <ul class="nav kofa-navbar-left">
+              <tal:primary_nav content="structure provider:primary_nav" />
+              <tal:primary_nav content="structure provider:primary_nav_student" />
+          </ul>
+          <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>
+          <span class="language"
+                tal:define="langs provider:languages"
+                tal:condition="langs">
+            <tal:languages
+                  tal:content="structure langs" /> |
+          </span>
+        </div>
+      </div>
+    </div>
+
+    <div class="container">
+      <ul class="student-info-bar" tal:condition="layout/isAuthenticated">
+        <tal:breadcrumbs
+              tal:condition="python: layout.isAuthenticated()"
+              tal:content="structure provider:breadcrumbs" />
+        <span tal:condition="layout/getStudentName" class="wfstatus">
+            <span tal:replace="python:context.student.student_id">
+              Id
+            </span>
+            |
+            <span tal:replace="python:context.student.translated_state">
+              State
+            </span>
+        </span>
+      </ul>
+      <div class="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-message error"
+          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>
+        <div class="pull-right">
+          <a i18n:translate="" href="#footer">Back to bottom</a>
+        </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>
+        <div id="footer" class="footer">
+          <div class="pull-right">
+            <a i18n:translate="" href="#">Back to top</a>
+          </div>
+          <div i18n:translate="">
+            Kofa - Student Management System
+          </div>
+          <div i18n:translate="">
+            Copyright &copy; WAeUP Group 2008-2013
+          </div>
+        </div>
+      </div>
+    </div>
+  </body>
+</html>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/subobjectlist.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/subobjectlist.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/subobjectlist.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/templates/unauthorized.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/unauthorized.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/unauthorized.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/templates/universitypage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/universitypage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/universitypage.pt	(revision 10886)
@@ -0,0 +1,1 @@
+<span tal:replace="structure view/frontpage">Frontpage</span>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/universityrss20feed.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/universityrss20feed.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/universityrss20feed.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/templates/usereditformpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/usereditformpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/usereditformpage.pt	(revision 10886)
@@ -0,0 +1,28 @@
+<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 name="password" type="password"  />
+        </td>
+      </tr>
+      <tr>
+        <td i18n:translate="" class="fieldname">Retype Password:</td>
+        <td>
+          <input 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/tags/0.2/src/waeup/kofa/browser/templates/userscontainerpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/userscontainerpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/userscontainerpage.pt	(revision 10886)
@@ -0,0 +1,29 @@
+<table i18n:domain="waeup.kofa" class="display dataTable">
+  <thead>
+    <tr>
+      <th i18n:translate="">User 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 tal:content="account/title">TITLE</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" type="submit" name="manage"
+              tal:attributes="value view/manage_button" />
+            <input class="btn" type="submit" name="delete"
+              tal:attributes="value view/delete_button" />
+        </form>
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/virtjobscontainerindex.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/virtjobscontainerindex.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/virtjobscontainerindex.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/templates/widgets.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/widgets.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/templates/widgets.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/tests/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/__init__.py	(revision 10886)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/forbidden_file.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/forbidden_file.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/forbidden_file.csv	(revision 10886)
@@ -0,0 +1,3 @@
+File with forbidden chracters
+
+This is character 160:  
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_async.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_async.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_async.py	(revision 10886)
@@ -0,0 +1,174 @@
+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 FunctionalTestCase, FunctionalLayer
+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_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/tags/0.2/src/waeup/kofa/browser/tests/test_browser.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_browser.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_browser.py	(revision 10886)
@@ -0,0 +1,424 @@
+## $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
+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
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+from waeup.kofa.university.faculty import Faculty
+from waeup.kofa.university.department import Department
+
+SAMPLE_FILE = os.path.join(os.path.dirname(__file__), 'test_file.csv')
+FORBIDDEN_FILE = os.path.join(os.path.dirname(__file__), 'forbidden_file.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_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
+
+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()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.cleanup_run_job() # make sure to remove temp dirs
+        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\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', 'mrportalsecret')
+        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', 'mrexportersecret')
+        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 = 'mrportalsecret'
+        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 = 'mrexportersecret'
+        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("configuration page").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,clearance_locked,clr_code,date_of_birth,email,'
+            'employer,firstname,lastname,matric_number,middlename,'
+            'nationality,officer_comment,perm_address,personal_updated,'
+            'phone,reg_number,sex,student_id,suspended,suspended_comment,'
+            'transcript_comment,'
+            'password,state,history,certcode,is_postgrad,current_level,'
+            'current_session\r\n')
+        self.cleanup_run_job()
+        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), 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)
+        return
+
+    def test_export_discard(self):
+        # we can discard a generated export result
+        self.trigger_export()
+        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='
+            in logcontent)
+        return
+
+    def test_skeleton_download(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.datacenter_path)
+        self.browser.getLink("Upload data").click()
+        self.assertTrue(
+            '<a href="skeleton?name=facultyprocessor">Download</a>'
+            in self.browser.contents)
+        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,title,title_prefix\r\n')
+        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
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_captcha.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_captcha.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_captcha.py	(revision 10886)
@@ -0,0 +1,289 @@
+## $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="..."></script>'
+            '<noscript>'
+            '<iframe src="..." height="300" width="500" '
+            '        frameborder="0"></iframe><br />'
+            '<textarea name="recaptcha_challenge_field" rows="3" '
+            '          cols="40"></textarea>'
+            '<input type="hidden" name="recaptcha_response_field" '
+            '       value="manual_challenge" /></noscript>',
+            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/tags/0.2/src/waeup/kofa/browser/tests/test_doctests.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_doctests.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_doctests.py	(revision 10886)
@@ -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([
+        'browser/batchprocessing.txt',
+        'browser/breadcrumbs.txt',
+        'browser/browser.txt',
+        ])
+    return suite
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_file.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_file.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_file.csv	(revision 10886)
@@ -0,0 +1,1 @@
+empty file
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_pdf.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_pdf.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_pdf.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/browser/tests/test_permissions.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_permissions.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/tests/test_permissions.py	(revision 10886)
@@ -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
+##
+"""
+Permission tests.
+
+Check accessibility of views, pages, viewlets.
+
+:test-layer: python
+
+"""
+import shutil
+import tempfile
+from zope.app.testing.functional import HTTPCaller as http
+from zope.component import createObject
+from zope.component.hooks import setSite, clearSite
+from zope.security.interfaces import Unauthorized
+from zope.testbrowser.testing import Browser
+from waeup.kofa.app import University
+from waeup.kofa.testing import (
+    FunctionalLayer, FunctionalTestCase, get_all_loggers, remove_new_loggers,
+    remove_logger)
+
+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(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.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 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
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/theming.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/theming.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/theming.py	(revision 10886)
@@ -0,0 +1,97 @@
+## $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 support themes.
+"""
+import grok
+from zope.component import getUtilitiesFor
+from zope.interface import Interface
+from zope.schema.interfaces import IVocabularyFactory
+from waeup.kofa.interfaces import SimpleKofaVocabulary
+from waeup.kofa.browser.interfaces import ITheme
+from waeup.kofa.browser.resources import (
+    waeuptheme_empty, waeup_base_css, favicon
+    )
+
+class KofaThemeBase(grok.GlobalUtility):
+    grok.implements(ITheme)
+    grok.name('base waeup theme')
+
+    description = u"Base Theme"
+
+    def getResources(self):
+        return [waeup_base_css, favicon]
+
+class KofaThemeEmpty(grok.GlobalUtility):
+    """A theme based on jQuery only.
+
+    XXX: crappy yet.
+    """
+    grok.implements(ITheme)
+    grok.name('empty theme')
+    description = u'Empty Theme'
+
+    def getResources(self):
+        return [waeuptheme_empty]
+
+def get_all_themes():
+    """Return all themes registered as `ITheme` somewhere.
+
+    Returns a list of ``(<NAME>, <THEME>)`` tuples where ``<NAME>`` is
+    the internal name under which a theme was registered and
+    ``<THEME>`` is the real theme implementing `ITheme`.
+    """
+    for name, theme in getUtilitiesFor(ITheme):
+        yield name, theme
+
+class ThemesVocabulary(grok.GlobalUtility):
+    """A vocabulary that provides all themes available.
+
+    A named global utility implementing
+
+      :class:`zope.schema.interfaces.IVocabularyFactory`
+
+    and registered under the name
+
+      'waeup.kofa.browser.theming.ThemesVocabulary'
+
+    Interface fields that wish to provide a list of available themes
+    can require a 'named vocabulary', i.e. set:
+
+      vocabulary = 'waeup.kofa.browser.theming.ThemesVocabulary'
+
+    and the vocabulary will deliver themes and their descriptive text
+    for instance in select boxes of forms.
+
+    The name is chosen so that the location of this vocabulary can be
+    found easily when used in an interface definition.
+    """
+    grok.implements(IVocabularyFactory)
+    grok.name('waeup.kofa.browser.theming.ThemesVocabulary')
+
+    def __call__(self, context):
+        """Deliver a vocabulary of available themes.
+
+        The `description`s of available themes are used as 'title' and
+        the utility names delivering the different themes as 'value'
+        and 'token'.
+        """
+        terms = [(theme.description, name)
+                 for name, theme in get_all_themes()]
+        vocab = SimpleKofaVocabulary(*terms)
+        return vocab
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/viewlets.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/viewlets.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/browser/viewlets.py	(revision 10886)
@@ -0,0 +1,840 @@
+## $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.i18n import translate
+from zope.component import getMultiAdapter, queryAdapter, getUtility
+from zope.interface import Interface
+from zope.location.interfaces import ISite
+from zope.traversing.browser import absoluteURL
+from waeup.kofa.browser.pages import (
+    UniversityPage, FacultiesContainerPage, DatacenterPage, FacultyPage,
+    DepartmentPage, CoursePage, CertificatePage, CertificateCoursePage,
+    UsersContainerPage, UserManageFormPage)
+from waeup.kofa.browser.interfaces import (
+    IFacultiesContainer, IFaculty, IDepartment, ICourse, ICertificate,
+    ICertificateCourse, IBreadcrumbContainer, IUniversity, IUsersContainer)
+from waeup.kofa.interfaces import (
+    IKofaUtils, IKofaObject, IKofaXMLExporter,
+    IKofaXMLImporter, IDataCenter, IUserAccount)
+from waeup.kofa.browser.layout import KofaPage, default_primary_nav_template
+from waeup.kofa.utils.helpers import get_user_account
+
+from waeup.kofa.interfaces import MessageFactory as _
+
+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')
+
+
+#
+# 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.
+        """
+        static = self.view.static
+        if static is None or static.get(self.icon, None) is None:
+            # In derived classes defined in other modules/packages
+            # than w.k.browser, ``static`` might refer to a static dir
+            # local to the derived class' module. As we often like to
+            # get the icons from here
+            # (i.e. waeup.kofa.browser/static), we set the directory
+            # resource appropiately.
+            #
+            # XXX: The hardcoding of 'w.k.browser' should be replaced
+            #      by something smarter.
+            #
+            # TODO: notes in here should go to general documentation.
+            static = queryAdapter(
+                self.request, Interface, name='waeup.kofa.browser')
+        return static[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
+
+# Problem with circular references. Disabled for now...
+# class ExportXMLAction(grok.Viewlet):
+#    grok.viewletmanager(ActionBar)
+#     #grok.view(Index)
+#     grok.order(98)
+#     grok.require('waeup.managePortal')
+
+#class ImportXMLAction(grok.Viewlet):
+#    grok.viewletmanager(ActionBar)
+#    #grok.view(Index)
+#    grok.order(99)
+#    grok.require('waeup.managePortal')
+#
+#    def update(self):
+#        # We cannot simply replace local sites.
+#        self.can_import = not ISite.providedBy(self.context)
+
+
+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'Portal Users')
+
+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.manageReports')
+
+    link = u'reports'
+    text = _(u'Reports')
+
+class MyPreferences(ManageLink):
+    """A link to personal preferences, placed in upper left box.
+    """
+    grok.order(6)
+    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(7)
+    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 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 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.showStudents')
+    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'
+    text = _('Update scores')
+    target = 'edit_scores'
+    grok.order(4)
+
+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 user')
+
+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')
+
+#
+# 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 = _('Portal Users')
+    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>'
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/catalog.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/catalog.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/catalog.py	(revision 10886)
@@ -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
+##
+"""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-like query that supports also ``apply``.
+    """
+    def apply(self, query):
+        """Get a catalog's BTree set of intids conforming to a query.
+        """
+        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 and ('session' in idx_name
+                or 'level' in idx_name):
+                value = int(value)
+            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/tags/0.2/src/waeup/kofa/catalog.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/catalog.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/catalog.txt	(revision 10886)
@@ -0,0 +1,235 @@
+:mod:`waeup.kofa.catalog` -- Cataloging support Kofa
+****************************************************
+
+.. module:: waeup.kofa.catalog
+
+Components that support cataloging and searching objects inside a
+Kofa site.
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+.. contents::
+
+Classes
+=======
+
+:class:`KofaQuery`
+------------------
+
+.. class:: KofaQuery()
+
+   .. attribute:: grok.implements(hurry.query.interfaces.IQuery)
+
+   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.
+
+   .. method:: searchResults(query)
+
+     Get the cataloged objects determined by ``query``.
+
+   .. method:: apply(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.
+
+Examples
+========
+
+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/tags/0.2/src/waeup/kofa/configuration.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/configuration.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/configuration.py	(revision 10886)
@@ -0,0 +1,73 @@
+## $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 session configuration objects.
+"""
+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 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/tags/0.2/src/waeup/kofa/configure.zcml
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/configure.zcml	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/configure.zcml	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/datacenter.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/datacenter.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/datacenter.py	(revision 10886)
@@ -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
+##
+"""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)
+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'
+
+    max_files = 100
+
+    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', '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 object data.
+        """
+        return os.path.join(self.storage, 'deleted')
+
+    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'):
+                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
+        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/tags/0.2/src/waeup/kofa/datacenter.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/datacenter.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/datacenter.txt	(revision 10886)
@@ -0,0 +1,290 @@
+Kofa 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 a location to put data of deleted
+items into:
+
+    >>> import os
+    >>> del_path = mydatacenter.deleted_path
+    >>> os.path.isdir(del_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', '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', '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', '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', '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', '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/tags/0.2/src/waeup/kofa/fees.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/fees.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/fees.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/frontpage.html
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/frontpage.html	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/frontpage.html	(revision 10886)
@@ -0,0 +1,103 @@
+<h1>Welcome to WAeUP.Kofa
+<br>
+<small>
+the Information and Registration Portal
+for Schools, Colleges and Universities
+</small>
+</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://twitter.github.com/bootstrap/">Bootstrap documentation</a>
+on GitHub 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/tags/0.2/src/waeup/kofa/frontpage.rst
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/frontpage.rst	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/frontpage.rst	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/ftesting.zcml
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/ftesting.zcml	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/ftesting.zcml	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/hostels/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/__init__.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/hostels/batching.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/batching.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/batching.py	(revision 10886)
@@ -0,0 +1,90 @@
+## $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 waeup.kofa.interfaces import IBatchProcessor, IGNORE_MARKER
+from waeup.kofa.utils.batching import BatchProcessor
+from waeup.kofa.hostels.interfaces import IHostel
+
+class HostelProcessor(BatchProcessor):
+    """A batch processor for IHostel objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'hostelprocessor'
+    grok.name(util_name)
+
+    name = u'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.
+        """
+        changed = []
+        for key, value in row.items():
+            # Skip fields to be ignored.
+            if value == IGNORE_MARKER:
+                continue
+            if not hasattr(obj, key):
+                continue
+            try:
+                evalvalue = eval(value)
+                if isinstance(evalvalue, list):
+                    value = evalvalue
+            except:
+                pass
+            setattr(obj, key, value)
+            log_value = getattr(value, 'code', value)
+            changed.append('%s=%s' % (key, log_value))
+
+        # Log actions...
+        location_field = self.location_fields[0]
+        items_changed = ', '.join(changed)
+        grok.getSite()['hostels'].logger.info(
+            '%s - %s - %s - updated: %s'
+            % (self.name, filename, row[location_field], items_changed))
+        return
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/browser.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/browser.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/browser.py	(revision 10886)
@@ -0,0 +1,377 @@
+## $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
+from waeup.kofa.browser.layout import (
+    KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
+    NullValidator)
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+from waeup.kofa.browser.resources import datepicker, datatable, tabs, warning
+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
+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
+
+def write_log_message(view, message):
+    ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
+    view.context.loggerInfo(ob_class, message)
+    return
+
+# 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:
+        write_log_message(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 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')]
+
+    def update(self):
+        tabs.need()
+        self.tab1 = self.tab2 = self.tab3 = self.tab4 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        warning.need()
+        datatable.need()
+        return super(HostelsContainerManagePage, self).update()
+
+    # 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)
+            write_log_message(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'))
+    def clearHostels(self, **data):
+        self.context.clearAllHostels()
+        self.flash(_('All hostels cleared.'))
+        write_log_message(self, 'all hostels cleared')
+        self.redirect(self.url(self.context, '@@manage')+'?tab2')
+        return
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        self.flash(_('Settings have been saved.'))
+        return
+
+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
+
+    @action(_('Create hostel'))
+    def addHostel(self, **data):
+        hostel = Hostel()
+        self.applyData(hostel, **data)
+        hostel.hostel_id = data[
+            'hostel_name'].lower().replace(' ','-').replace('_','-')
+        try:
+            self.context.addHostel(hostel)
+        except KeyError:
+            self.flash(_('The hostel already exists.'))
+            return
+        self.flash(_('Hostel created.'))
+        write_log_message(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
+
+    @property
+    def students_url(self):
+        return self.url(grok.getSite(),'students')
+
+    def update(self):
+        datepicker.need() # Enable jQuery datepicker in date fields.
+        tabs.need()
+        datatable.need()
+        warning.need()
+        self.tab1 = self.tab2 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        super(HostelManageFormPage, self).update()
+        return
+
+    @action(_('Save'))
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+    @action(_('Update all beds'))
+    def updateBeds(self, **data):
+        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)
+        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)
+        write_log_message(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.'))
+            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)
+            self.flash(_('Successfully switched beds: ${a}',
+                mapping = {'a':m_translated}))
+            write_log_message(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.'))
+            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}))
+            write_log_message(self, 'released: %s' % message)
+            self.redirect(self.url(self.context, '@@manage')+'?tab2')
+        else:
+            self.flash(_('No allocated bed selected.'))
+            self.redirect(self.url(self.context, '@@manage')+'?tab2')
+        return
+
+    @jsaction(_('Clear hostel'))
+    def clearHostel(self, **data):
+        self.context.clearHostel()
+        self.flash(_('Hostel cleared.'))
+        write_log_message(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
+
+    @action(_('Save'))
+    def save(self, **data):
+        if data['owner'] == NOT_OCCUPIED:
+            self.flash(_('No valid student id.'))
+            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/tags/0.2/src/waeup/kofa/hostels/browser_templates/containermanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/browser_templates/containermanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/browser_templates/containermanagepage.pt	(revision 10886)
@@ -0,0 +1,60 @@
+<form action="." tal:attributes="action request/URL"
+    i18n:domain="waeup.kofa"
+    method="POST" enctype="multipart/form-data">
+
+
+  <ul class="tabs" data-tabs="tabs">
+    <li tal:attributes="class view/tab1">
+    <a href="#tab-1">
+      <span i18n:translate="">Settings
+      </span></a>
+    </li>
+    <li tal:attributes="class view/tab2">
+    <a href="#tab-2" i18n:translate="">Hostels</a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+      <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="tab-2" tal:attributes="class view/tab2"> <br />
+      <table class="display dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">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>
+              <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/tags/0.2/src/waeup/kofa/hostels/browser_templates/containerpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/browser_templates/containerpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/browser_templates/containerpage.pt	(revision 10886)
@@ -0,0 +1,38 @@
+<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">
+  <thead>
+    <tr>
+      <th i18n:translate="">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/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/tags/0.2/src/waeup/kofa/hostels/browser_templates/hostelmanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/browser_templates/hostelmanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/browser_templates/hostelmanagepage.pt	(revision 10886)
@@ -0,0 +1,89 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <ul class="tabs" data-tabs="tabs">
+    <li tal:attributes="class view/tab1"><a href="#tab-1">
+      <span i18n:translate="">Hostel Data</span></a>
+    </li>
+    <li tal:attributes="class view/tab2"><a href="#tab-2">
+      <span i18n:translate="">Beds</span></a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+      <table class="form-table">
+      <tbody>
+        <tal:widgets content="structure provider:widgets" />
+        <tr>
+          <td class="fieldname" i18n:translate="" >
+            Reserved Beds:
+          </td>
+          <td>
+            <tal:repeat repeat="bed context/beds_reserved" >
+                <span tal:replace='bed'>BED</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+            </tal:repeat>
+          </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>
+    </div>
+
+    <div id="tab-2" tal:attributes="class view/tab2">
+      <h3>Beds</h3>
+      <table class="display 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>
+
+      <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/tags/0.2/src/waeup/kofa/hostels/browser_templates/hostelpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/browser_templates/hostelpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/browser_templates/hostelpage.pt	(revision 10886)
@@ -0,0 +1,15 @@
+<table class="form-table" i18n:domain="waeup.kofa">
+  <tbody>
+    <tal:widgets content="structure provider:widgets" />
+    <tr>
+      <td class="fieldname" i18n:translate="" >
+        Reserved Beds:
+      </td>
+      <td>
+        <tal:repeat repeat="bed context/beds_reserved" >
+            <span tal:replace='bed'>BED</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+        </tal:repeat>
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/catalog.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/catalog.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/catalog.py	(revision 10886)
@@ -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
+##
+"""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_id = index.Field(attribute='bed_id')
+    #bed_number = index.Field(attribute='bed_number')
+    bed_type = index.Field(attribute='bed_type')
+    owner = index.Field(attribute='owner')
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/container.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/container.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/container.py	(revision 10886)
@@ -0,0 +1,86 @@
+## $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 waeup.kofa.hostels.interfaces import IHostelsContainer, IHostel
+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 archive(self, id=None):
+        raise NotImplementedError()
+
+    def clear(self, id=None, archive=True):
+        raise NotImplementedError()
+
+    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
+
+    logger_name = 'waeup.kofa.${sitename}.hostels'
+    logger_filename = 'hostels.log'
+
+    def logger_info(self, ob_class, target, comment=None):
+        """Get the logger's info method.
+        """
+        self.logger.info('%s - %s - %s' % (
+                ob_class, target, comment))
+        return
+
+    def loggerInfo(self, ob_class, comment=None):
+        target = self.__name__
+        return self.logger_info(ob_class,target,comment)
+
+    @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
+
+HostelsContainer = attrs_to_fields(HostelsContainer)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/export.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/export.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/export.py	(revision 10886)
@@ -0,0 +1,74 @@
+## $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):
+    """Exporter for hostels.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('hostels')
+
+    #: Fieldnames considered by this exporter
+    fields = tuple(sorted(iface_names(IHostel))) 
+
+    #: The title under which this exporter will be displayed
+    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):
+    """Exporter for courses.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('beds')
+
+    #: Fieldnames considered by this exporter
+    fields = tuple(sorted(iface_names(IBed))) + (
+        'hall', 'block', 'room', 'bed', 'special_handling', 'sex', 'bt')
+
+    #: The title under which this exporter will be displayed
+    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/tags/0.2/src/waeup/kofa/hostels/hostel.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/hostel.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/hostel.py	(revision 10886)
@@ -0,0 +1,301 @@
+## $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
+from zope.component.interfaces import IFactory
+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
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.helpers import now
+
+class Hostel(grok.Container):
+    """This is a hostel.
+    """
+    grok.implements(IHostel)
+    grok.provides(IHostel)
+
+    def loggerInfo(self, ob_class, comment=None):
+        target = self.__name__
+        return grok.getSite()['hostels'].logger_info(ob_class,target,comment)
+
+    @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',[])
+        beds_reserved = getattr(self,'beds_reserved',[])
+        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)
+        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*100 + room
+                        bt = 'all'
+                        if '%s_%s_%s' % (block,room_nr,bed) in beds_reserved:
+                            bt = "reserved"
+                        elif 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 beds
+                            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 = 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
+
+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)
+
+            # Comment of Martijn:
+            # If you have a non-Persistent subobject (like a list) and
+            # you change it, you need to manually flag the persistence
+            # machinery on the object that its subobject changed, with
+            # _p_changed. This is only necessary if some of the
+            # objects are not sublclasses of Persistent. For common
+            # built-in collections in Python such as list and
+            # dictionary there are replacements (PersistentList,
+            # PersistentMapping), and more advanced building blocks
+            # for indexes (BTrees), that don't have this issue.
+            #hostel._p_changed = True
+
+            # Henrik: Unfortunately, this doesn't work in our case.
+            # After restarting the portal,
+            # added or removed list items are gone. The only way to ensure
+            # persistance is to reassign the list attribute.
+            br = hostel.beds_reserved
+            br.remove(bed_string)
+            hostel.beds_reserved = br
+            message = _(u'unreserved')
+        else:
+            bt = u'%s_%s_reserved' % (sh, sex)
+            # See comment above
+            br = hostel.beds_reserved
+            br.append(bed_string)
+            hostel.beds_reserved = br
+            message = _(u'reserved')
+        self.bed_type = bt
+        notify(grok.ObjectModifiedEvent(self))
+        return message
+
+    def releaseBed(self):
+        if self.owner == NOT_OCCUPIED:
+            return
+        else:
+            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 loggerInfo(self, ob_class, comment=None):
+        target = self.__name__
+        return grok.getSite()['hostels'].logger_info(ob_class,target,comment)
+
+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)
+
+
+@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))
+
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/interfaces.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/interfaces.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/interfaces.py	(revision 10886)
@@ -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
+##
+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
+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)
+
+class IHostelsContainer(IKofaObject):
+    """A container for all kind of hostel objects.
+
+    """
+
+    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,
+            ),
+        default = [],
+        )
+
+    def clearAllHostels():
+        """Clear all hostels.
+        """
+
+class IHostel(IKofaObject):
+    """A base representation of hostels.
+
+    """
+
+    bed_statistics = Attribute('Number of booked and total beds')
+
+    def loggerInfo(ob_class, comment):
+        """Adds an INFO message to the log file
+        """
+
+    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,
+        )
+
+    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,
+        )
+
+    beds_reserved = schema.List(
+        title = _(u'Reserved Beds'),
+        value_type = schema.TextLine(
+            default = u'',
+            required = False,
+        ),
+        required = True,
+        readonly = False,
+        default = [],
+        )
+
+    blocks_for_female = schema.List(
+        title = _(u'Blocks for Female Students'),
+        value_type = schema.Choice(
+            vocabulary = blocks
+            ),
+        )
+
+    blocks_for_male = schema.List(
+        title = _(u'Blocks for Male Students'),
+        value_type = schema.Choice(
+            vocabulary = blocks
+            ),
+        )
+
+    beds_for_pre= schema.List(
+        title = _(u'Beds for Pre-Study Students'),
+        value_type = schema.Choice(
+            vocabulary = bed_letters
+            ),
+        )
+
+    beds_for_fresh = schema.List(
+        title = _(u'Beds for Fresh Students'),
+        value_type = schema.Choice(
+            vocabulary = bed_letters
+            ),
+        )
+
+    beds_for_returning = schema.List(
+        title = _(u'Beds for Returning Students'),
+        value_type = schema.Choice(
+            vocabulary = bed_letters
+            ),
+        )
+
+    beds_for_final = schema.List(
+        title = _(u'Beds for Final Year Students'),
+        value_type = schema.Choice(
+            vocabulary = bed_letters
+            ),
+        )
+
+    beds_for_all = schema.List(
+        title = _(u'Beds without category'),
+        value_type = schema.Choice(
+            vocabulary = bed_letters
+            ),
+        )
+
+    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.'))
+
+class IBed(IKofaObject):
+    """A base representation of beds.
+
+    """
+
+    coordinates = Attribute('The coordinates of the bed from bed_id')
+
+    def loggerInfo(ob_class, comment):
+        """Adds an INFO message to the log file
+        """
+
+    def bookBed(student_id):
+        """Book a bed for a student.
+        """
+
+    def switchReservation():
+        """Reserves bed or relases reserved bed respectively.
+        """
+
+    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}))
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/permissions.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/permissions.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/permissions.py	(revision 10886)
@@ -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
+##
+"""
+Permissions for the accommdation section.
+"""
+import grok
+
+# Accommodation section permissions
+
+class ViewHostels(grok.Permission):
+    grok.name('waeup.viewHostels')
+
+class ManageHostels(grok.Permission):
+    grok.name('waeup.manageHostels')
+
+# Site Roles
+class AccommodationOfficer(grok.Role):
+    grok.name('waeup.AccommodationOfficer')
+    grok.title(u'Accommodation Officer')
+    grok.permissions('waeup.viewHostels', 'waeup.manageHostels')
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/sample_hostel_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/sample_hostel_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/sample_hostel_data.csv	(revision 10886)
@@ -0,0 +1,11 @@
+beds_for_all,beds_for_final,beds_for_fresh,beds_for_pre,beds_for_returning,beds_reserved,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,100,regular
+[],[],[],[],[],[],[],['A'],1,stores-32,Stores 32,16,10,regular
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/tests.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/tests.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/tests.py	(revision 10886)
@@ -0,0 +1,545 @@
+## $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.hostels.interfaces import (
+    IHostelsContainer, IHostel, IBed)
+from waeup.kofa.hostels.container import HostelsContainer
+from waeup.kofa.hostels.hostel import Hostel, Bed
+from waeup.kofa.hostels.batching import HostelProcessor
+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(',')
+
+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)
+            )
+        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()
+        self.assertRaises(
+            NotImplementedError, container.archive)
+        self.assertRaises(
+            NotImplementedError, container.clear)
+        # 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'a_b_c'
+        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 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'a_b_c', u'a_b_c'))
+        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>
+<li>Block B</li></ul>...'''
+        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()
+        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 bed
+        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')
+        self.assertTrue('A_101_A&nbsp;&nbsp;' in self.browser.contents)
+        # 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]
+        self.assertEqual(results,
+            [(u'hall-1_B_101_D', u'regular_female_all'),])
+        # Unreserve bed
+        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()
+        assert self.app['hostels']['hall-1'][
+            'hall-1_A_101_D'].bed_type == 'regular_female_all'
+        self.assertFalse(expected in self.browser.contents)
+        # 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
+        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 --')
+        # If we release a free be, 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)
+        # 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
+
+    def test_clear_hostels(self):
+        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,beds_reserved,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,a_b_c,,'
+            'hall,block,room,bed,a,b,c\r\n'
+            )
+        return
+
+    def tearDown(self):
+        super(ExportTests, self).tearDown()
+        clearSite()
+        shutil.rmtree(os.path.dirname(self.outfile))
+
+class HostelProcessorTest(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, "
+            "beds_for_final=['A', 'B'], rooms_per_floor=32, "
+            "blocks_for_male=[], 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'], beds_for_all=[], beds_reserved=[]"
+            in logcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+        shutil.rmtree(self.workdir)
+        return
+
+    def test_import_update(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)
+        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, "
+            "beds_for_final=['A', 'B'], rooms_per_floor=32, "
+            "blocks_for_male=[], 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'], beds_for_all=[], beds_reserved=[]"
+            in logcontent)
+        shutil.rmtree(os.path.dirname(fin_file))
+        shutil.rmtree(self.workdir)
+        return
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/vocabularies.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/vocabularies.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/hostels/vocabularies.py	(revision 10886)
@@ -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
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.university.vocabularies import ContextualDictSourceFactoryBase
+
+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/tags/0.2/src/waeup/kofa/image/README.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/image/README.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/image/README.txt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/image/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/image/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/image/__init__.py	(revision 10886)
@@ -0,0 +1,10 @@
+"""Image file components.
+
+Includings schemas, widgets and the content components.
+"""
+from waeup.kofa.image.image import KofaImageFile, createKofaImageFile
+
+__all__ = [
+    "KofaImageFile",
+    "createKofaImageFile",
+    ]
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/__init__.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/image/browser/tests/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/tests/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/tests/__init__.py	(revision 10886)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/tests/image.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/tests/image.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/tests/image.txt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/image/browser/tests/test_image.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/tests/test_image.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/tests/test_image.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/image/browser/tests/test_views.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/tests/test_views.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/tests/test_views.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/image/browser/views.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/views.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/views.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/image/browser/widget.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/widget.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/image/browser/widget.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/image/image.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/image/image.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/image/image.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/image/interfaces.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/image/interfaces.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/image/interfaces.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/image/schema.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/image/schema.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/image/schema.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/image/tests.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/image/tests.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/image/tests.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/imagestorage.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/imagestorage.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/imagestorage.py	(revision 10886)
@@ -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 tratment 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/tags/0.2/src/waeup/kofa/index.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/index.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/index.txt	(revision 10886)
@@ -0,0 +1,30 @@
+Welcome to waeup documentation!
+*******************************
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+
+   README
+   doc/developernotes
+
+API documentation
+=================
+
+.. toctree::
+   :maxdepth: 1
+
+   app
+   catalog
+   accesscodes/api.txt
+   jambtables/api.txt
+   university/api.txt
+   utils/api.txt
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/interfaces.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/interfaces.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/interfaces.py	(revision 10886)
@@ -0,0 +1,1357 @@
+## $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
+
+_ = MessageFactory = zope.i18nmessageid.MessageFactory('waeup.kofa')
+
+DELETION_MARKER = 'XXX'
+IGNORE_MARKER = '<IGNORE>'
+WAEUP_KEY = 'waeup.kofa'
+VIRT_JOBS_CONTAINER_NAME = 'jobs'
+
+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'
+TRANSCRIPT = 'transcript requested'
+
+
+#: 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(1995, 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'), TRANSCRIPT),
+    )
+
+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]
+
+# 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 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 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_DICT = 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")
+    INT_PHONE_PREFIXES = Attribute(
+        "Dict of international phone number prefixes")
+
+    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 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.
+        """
+
+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")
+
+
+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,)
+
+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,
+        )
+
+
+
+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.
+        """
+
+
+class IUsersContainer(IKofaObject):
+    """A container for users (principals).
+
+    These users are used for authentication purposes.
+    """
+
+    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,
+        )
+
+    skin = schema.Choice(
+        title = _(u'Skin'),
+        default = u'gray waeup theme',
+        vocabulary = 'waeup.kofa.browser.theming.ThemesVocabulary',
+        required = True,
+        )
+
+    frontpage = schema.Text(
+        title = _(u'Content in HTML format'),
+        required = False,
+        default = default_html_frontpage,
+        )
+
+    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,
+        )
+
+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,
+        )
+
+    application_fee = schema.Float(
+        title = _(u'Application Fee'),
+        default = 0.0,
+        required = False,
+        )
+
+    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,
+        )
+
+    transcript_fee = schema.Float(
+        title = _(u'Transcript Fee'),
+        default = 0.0,
+        required = False,
+        )
+
+    clearance_enabled = schema.Bool(
+        title = _(u'Clearance enabled'),
+        default = False,
+        )
+
+    def getSessionString():
+        """Returns 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 were data about deleted objects should be stored.'
+        )
+
+    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/tags/0.2/src/waeup/kofa/language.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/language.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/language.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/locales.zcml
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/locales.zcml	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/locales.zcml	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/locales/de/LC_MESSAGES/waeup.kofa.po
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/de/LC_MESSAGES/waeup.kofa.po	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/de/LC_MESSAGES/waeup.kofa.po	(revision 10886)
@@ -0,0 +1,6727 @@
+# #############################################################################
+#
+# 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: Thu Jun 27 09:36:46 2013\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:102 waeup/kofa/accesscodes/browser.py:115
+#: waeup/kofa/applicants/browser.py:262 waeup/kofa/applicants/browser.py:351
+#: waeup/kofa/applicants/browser.py:352 waeup/kofa/applicants/browser.py:472
+#: waeup/kofa/browser/pages.py:636 waeup/kofa/browser/pages.py:846
+#: waeup/kofa/browser/pages.py:872 waeup/kofa/browser/pages.py:939
+#: waeup/kofa/browser/pages.py:1171 waeup/kofa/browser/pages.py:1201
+#: waeup/kofa/browser/pages.py:1304 waeup/kofa/browser/pages.py:1535
+#: waeup/kofa/browser/pages.py:1701 waeup/kofa/browser/pages.py:1725
+#: waeup/kofa/browser/pages.py:1756 waeup/kofa/browser/pages.py:1784
+#: waeup/kofa/browser/pages.py:1785 waeup/kofa/browser/pages.py:1828
+#: waeup/kofa/browser/pages.py:1873 waeup/kofa/browser/pages.py:1918
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1921
+#: waeup/kofa/browser/pages.py:1996 waeup/kofa/browser/pages.py:2045
+#: waeup/kofa/browser/pages.py:2086 waeup/kofa/browser/pages.py:2122
+#: waeup/kofa/browser/pages.py:2161 waeup/kofa/browser/pages.py:2163
+#: waeup/kofa/browser/pages.py:2196 waeup/kofa/browser/pages.py:2247
+#: waeup/kofa/browser/pages.py:2304 waeup/kofa/students/browser.py:965
+#: waeup/kofa/students/browser.py:966 waeup/kofa/students/browser.py:1341
+#: waeup/kofa/students/browser.py:1343 waeup/kofa/students/browser.py:1500
+#: waeup/kofa/students/browser.py:1659 waeup/kofa/students/browser.py:1697
+#: waeup/kofa/students/browser.py:1730
+msgid "Cancel"
+msgstr "Abbruch"
+
+#: waeup/kofa/accesscodes/browser.py:104
+msgid "Batch creation cancelled."
+msgstr "Zugangscodeerzeugung abgebrochen."
+
+#: waeup/kofa/accesscodes/browser.py:114
+msgid "Reimport"
+msgstr "Reimportieren"
+
+#: waeup/kofa/accesscodes/browser.py:117
+msgid "Reimport Access Code Batches"
+msgstr "Zugangscodeserie reimportieren"
+
+#: waeup/kofa/accesscodes/browser.py:122
+msgid "Reimport cancelled."
+msgstr "Reimport abgebrochen."
+
+#: waeup/kofa/accesscodes/browser.py:128
+msgid "No file chosen. Action cancelled."
+msgstr "Keine Datei ausgewählt, Aktion abgebrochen."
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:138
+msgid "This batch already exists: ${a}"
+msgstr "Diese Serie existiert bereits: ${a}"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:141
+msgid "Successfully reimported: ${a}"
+msgstr "Erfolgreich reimportiert: ${a}"
+
+#: waeup/kofa/accesscodes/browser.py:154
+msgid "Search and Manage Access Codes"
+msgstr "Suche und verwalte Zugangscodes"
+
+#: waeup/kofa/accesscodes/browser.py:155 waeup/kofa/applicants/browser.py:85
+#: waeup/kofa/applicants/browser.py:113 waeup/kofa/browser/pages.py:725
+#: waeup/kofa/browser/pages.py:1495 waeup/kofa/browser/viewlets.py:200
+#: waeup/kofa/students/browser.py:258 waeup/kofa/students/browser.py:299
+msgid "Search"
+msgstr "Suchen"
+
+#: waeup/kofa/accesscodes/browser.py:156
+msgid "Disable ACs"
+msgstr "Zugangscode deaktivieren"
+
+#: waeup/kofa/accesscodes/browser.py:157
+msgid "Enable ACs"
+msgstr "Zugangscode aktivieren"
+
+#: waeup/kofa/accesscodes/browser.py:182 waeup/kofa/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr "deaktiviert"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:184
+msgid "${a} disabled."
+msgstr "${a} deaktiviert"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:188
+msgid "${a}: Disable transition not allowed."
+msgstr "${a}: Deaktivierung nicht möglich."
+
+#: waeup/kofa/accesscodes/browser.py:192
+msgid "re-enabled"
+msgstr "reaktiviert"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:194
+msgid "${a} (re-)enabled."
+msgstr "${a} (re-)aktiviert."
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:198
+msgid "${a}: Re-enable transition not allowed."
+msgstr "${a}: Reaktivieren nich möglich."
+
+#: waeup/kofa/accesscodes/browser.py:237
+msgid "Add Access Code Batch"
+msgstr "Zugangscodeserie hinzufügen"
+
+#: waeup/kofa/accesscodes/browser.py:246
+msgid "Reimport Access Code Batch"
+msgstr "Zugangscodeserie reimportieren"
+
+#: waeup/kofa/accesscodes/browser.py:254
+msgid "Search Access Codes"
+msgstr "Zugangscode suchen"
+
+#: waeup/kofa/accesscodes/browser.py:263
+msgid "Access Codes"
+msgstr "Zugangscodes"
+
+#: waeup/kofa/accesscodes/browser.py:44
+msgid "Archive"
+msgstr "Archivieren"
+
+#: waeup/kofa/accesscodes/browser.py:45
+msgid "Archive and delete"
+msgstr "Archivieren und löschen"
+
+#: waeup/kofa/accesscodes/browser.py:47 waeup/kofa/accesscodes/browser.py:208
+#: waeup/kofa/accesscodes/browser.py:228
+msgid "Access Code Batches"
+msgstr "Zugangscodeserien"
+
+#: waeup/kofa/accesscodes/browser.py:54
+msgid "No batch selected."
+msgstr "Keine Serie ausgewählt."
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:62
+msgid "Archived ${a} (${b})"
+msgstr "${a} (${b}) archiviert"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:70
+msgid "Deleted batch ${a}"
+msgstr "Serie ${a} gelöscht"
+
+#: waeup/kofa/accesscodes/browser.py:79
+msgid "Create Access Code Batch"
+msgstr "Zugangscodeserie erzeugen"
+
+#: waeup/kofa/accesscodes/browser.py:85
+msgid "Create batch"
+msgstr "Serie erzeugen"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:93
+msgid "Batch created (${a} entries)"
+msgstr "Serie ${a} mit Einträgen erzeugt."
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:95
+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:23
+msgid "Serial"
+msgstr "Seriennummer"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:24
+msgid "AC"
+msgstr "Zugangscode"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:26
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:48
+msgid "Owner"
+msgstr "Eigentümer"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:5
+msgid "for access codes"
+msgstr "nach Zugangscodes"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:7
+msgid "with PIN"
+msgstr "mit PIN"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:8
+msgid "with serial"
+msgstr "mit Seriennummer"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:9
+msgid "with message term"
+msgstr "mit Mitteilung"
+
+#: 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:119
+msgid "Applicant has not yet been admitted."
+msgstr "Antrafsteller wurde noch nicht zugelassen."
+
+#: waeup/kofa/applicants/applicant.py:125
+msgid "Registration Number exists."
+msgstr "Regsistrierungsnummer existiert."
+
+#: waeup/kofa/applicants/applicant.py:128
+msgid "No course admitted provided."
+msgstr "Es wurde noch kein Studiengang zugewiesen."
+
+#. Default: ""
+#: waeup/kofa/applicants/applicant.py:160
+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:1002
+msgid "1st and 2nd choice must be different."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1020
+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:1026
+msgid "Form has been submitted."
+msgstr "Das Formular wurde eingereicht."
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:1069
+msgid "Apply for ${a}"
+msgstr "Registrierung für ${a}"
+
+#: waeup/kofa/applicants/browser.py:1074
+msgid "Outside application period."
+msgstr "Außerhalb des Antragszeitraums."
+
+#: waeup/kofa/applicants/browser.py:1089 waeup/kofa/browser/pages.py:2353
+#: waeup/kofa/students/browser.py:2700
+msgid "Send login credentials to email address"
+msgstr "Zugangsdaten an Email-Adresse senden"
+
+#: waeup/kofa/applicants/browser.py:1112 waeup/kofa/students/browser.py:2717
+msgid "An error occurred."
+msgstr "Es ein Fehler auf."
+
+#: waeup/kofa/applicants/browser.py:1118 waeup/kofa/applicants/browser.py:1130
+msgid "No application record found."
+msgstr "Es wurde kein passender Datensatz gefunden."
+
+#: waeup/kofa/applicants/browser.py:112 waeup/kofa/applicants/viewlets.py:90
+msgid "Search applicants"
+msgstr "Suche nach Antragstellern"
+
+#: waeup/kofa/applicants/browser.py:1122 waeup/kofa/students/browser.py:2726
+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:1141
+msgid "You have successfully been registered for the"
+msgstr "Sie haben sich erfolgreich registriert für"
+
+#: waeup/kofa/applicants/browser.py:1158
+msgid "Your registration was successful."
+msgstr "Ihre Registrierung war erfolgreich."
+
+#: waeup/kofa/applicants/browser.py:128 waeup/kofa/students/browser.py:276
+#: waeup/kofa/students/browser.py:319
+msgid "Empty search string"
+msgstr "Leerer Suchtext"
+
+#: waeup/kofa/applicants/browser.py:133
+msgid "No applicant found."
+msgstr "Es wurde kein Antragsteller gefunden."
+
+#: waeup/kofa/applicants/browser.py:141 waeup/kofa/applicants/viewlets.py:99
+msgid "Manage application section"
+msgstr "Antragstellungsverfahren verwalten"
+
+#: waeup/kofa/applicants/browser.py:144 waeup/kofa/applicants/browser.py:219
+#: waeup/kofa/applicants/browser.py:351 waeup/kofa/applicants/browser.py:404
+#: waeup/kofa/applicants/browser.py:795 waeup/kofa/applicants/browser.py:841
+#: waeup/kofa/applicants/browser.py:949 waeup/kofa/applicants/browser.py:952
+#: waeup/kofa/applicants/browser.py:996 waeup/kofa/browser/pages.py:607
+#: waeup/kofa/browser/pages.py:764 waeup/kofa/browser/pages.py:787
+#: waeup/kofa/browser/pages.py:866 waeup/kofa/browser/pages.py:1533
+#: waeup/kofa/browser/pages.py:1784 waeup/kofa/browser/pages.py:1824
+#: waeup/kofa/browser/pages.py:1918 waeup/kofa/browser/pages.py:1967
+#: waeup/kofa/browser/pages.py:2118 waeup/kofa/browser/pages.py:2161
+#: waeup/kofa/browser/pages.py:2182 waeup/kofa/hostels/browser.py:131
+#: waeup/kofa/hostels/browser.py:176 waeup/kofa/hostels/browser.py:241
+#: waeup/kofa/hostels/browser.py:265 waeup/kofa/hostels/browser.py:360
+#: waeup/kofa/students/browser.py:542 waeup/kofa/students/browser.py:595
+#: waeup/kofa/students/browser.py:770 waeup/kofa/students/browser.py:872
+#: waeup/kofa/students/browser.py:965 waeup/kofa/students/browser.py:993
+#: waeup/kofa/students/browser.py:1341 waeup/kofa/students/browser.py:1386
+#: waeup/kofa/students/browser.py:2173 waeup/kofa/students/browser.py:2188
+#: waeup/kofa/students/browser.py:2311
+msgid "Save"
+msgstr "Speichern"
+
+#: waeup/kofa/applicants/browser.py:145 waeup/kofa/applicants/browser.py:175
+#: waeup/kofa/applicants/browser.py:352 waeup/kofa/applicants/browser.py:423
+#: waeup/kofa/browser/pages.py:767 waeup/kofa/browser/pages.py:809
+#: waeup/kofa/browser/pages.py:893 waeup/kofa/browser/pages.py:1701
+#: waeup/kofa/browser/pages.py:1712 waeup/kofa/browser/pages.py:1785
+#: waeup/kofa/browser/pages.py:1816 waeup/kofa/hostels/browser.py:134
+#: waeup/kofa/hostels/browser.py:149 waeup/kofa/students/browser.py:300
+#: waeup/kofa/students/browser.py:1840 waeup/kofa/students/browser.py:1853
+msgid "Remove selected"
+msgstr "Ausgewählte Objekte löschen"
+
+#: waeup/kofa/applicants/browser.py:145 waeup/kofa/applicants/browser.py:200
+#: waeup/kofa/applicants/browser.py:231 waeup/kofa/applicants/browser.py:241
+msgid "Add applicants container"
+msgstr "Ordner für Antragsteller hinzufügen"
+
+#: waeup/kofa/applicants/browser.py:146 waeup/kofa/applicants/browser.py:209
+#: waeup/kofa/applicants/browser.py:354 waeup/kofa/applicants/browser.py:481
+#: waeup/kofa/browser/pages.py:1786 waeup/kofa/browser/pages.py:1842
+#: waeup/kofa/browser/pages.py:1922 waeup/kofa/browser/pages.py:2005
+#: waeup/kofa/browser/pages.py:2164 waeup/kofa/browser/pages.py:2215
+msgid "Remove selected local roles"
+msgstr "Ausgewählte lokale Rollen löschen"
+
+#: waeup/kofa/applicants/browser.py:147 waeup/kofa/applicants/browser.py:205
+#: waeup/kofa/applicants/browser.py:355 waeup/kofa/applicants/browser.py:477
+#: waeup/kofa/browser/pages.py:1787 waeup/kofa/browser/pages.py:1838
+#: waeup/kofa/browser/pages.py:1923 waeup/kofa/browser/pages.py:2001
+#: waeup/kofa/browser/pages.py:2165 waeup/kofa/browser/pages.py:2211
+msgid "Add local role"
+msgstr "Lokale Rolle hinzufügen"
+
+#: waeup/kofa/applicants/browser.py:148
+msgid "Applicants Containers"
+msgstr "Ordner für Antragsteller"
+
+#: waeup/kofa/applicants/browser.py:181
+msgid "No container selected!"
+msgstr "Kein Container ausgewählt."
+
+#: waeup/kofa/applicants/browser.py:192 waeup/kofa/applicants/browser.py:440
+#: waeup/kofa/applicants/browser.py:896
+msgid "Could not delete:"
+msgstr "Konnte nicht gelöscht werden:"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:195 waeup/kofa/applicants/browser.py:443
+#: waeup/kofa/applicants/browser.py:899 waeup/kofa/browser/pages.py:153
+#: waeup/kofa/students/browser.py:346 waeup/kofa/students/browser.py:1055
+#: waeup/kofa/students/browser.py:1407 waeup/kofa/students/browser.py:1602
+#: waeup/kofa/students/browser.py:1873 waeup/kofa/students/browser.py:2549
+msgid "Successfully removed: ${a}"
+msgstr "Erfolgreich entfernt: ${a}"
+
+#: waeup/kofa/applicants/browser.py:223 waeup/kofa/applicants/browser.py:418
+#: waeup/kofa/applicants/browser.py:869 waeup/kofa/applicants/browser.py:1005
+#: waeup/kofa/browser/pages.py:195 waeup/kofa/browser/pages.py:2295
+#: waeup/kofa/hostels/browser.py:55 waeup/kofa/students/browser.py:100
+#: waeup/kofa/students/browser.py:564
+msgid "Form has been saved."
+msgstr "Das Formular wurde gespeichert."
+
+#: waeup/kofa/applicants/browser.py:250
+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:258
+msgid "Added:"
+msgstr "Hinzugefügt:"
+
+#: waeup/kofa/applicants/browser.py:270 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:353 waeup/kofa/applicants/browser.py:448
+msgid "Create students from selected"
+msgstr "Wandel ausgewählte Antragsteller in Studenten um"
+
+#: waeup/kofa/applicants/browser.py:361
+msgid "Manage applicants container"
+msgstr "Ordner für Antragsteller verwalten"
+
+#: waeup/kofa/applicants/browser.py:429 waeup/kofa/applicants/browser.py:454
+msgid "No applicant selected!"
+msgstr "Nichts ausgewählt!"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:465 waeup/kofa/applicants/browser.py:614
+msgid "${a} students successfully created."
+msgstr "Student ${a} erfolgreich erzeugt."
+
+#: waeup/kofa/applicants/browser.py:468 waeup/kofa/applicants/browser.py:617
+msgid "No student could be created."
+msgstr "Es konnte kein Student erzeugt werden."
+
+#: waeup/kofa/applicants/browser.py:495 waeup/kofa/applicants/viewlets.py:115
+msgid "Add applicant"
+msgstr "Antragsteller hinzufügen"
+
+#: waeup/kofa/applicants/browser.py:498
+msgid "Create application record"
+msgstr "Antrag hinzufügen"
+
+#: waeup/kofa/applicants/browser.py:503
+msgid "Applicant record created."
+msgstr "Antrag wurde erzeugt."
+
+#: waeup/kofa/applicants/browser.py:517
+msgid "Applicant"
+msgstr "Antragsteller"
+
+#: waeup/kofa/applicants/browser.py:541 waeup/kofa/students/browser.py:438
+msgid "set"
+msgstr "gesetzt"
+
+#: waeup/kofa/applicants/browser.py:542 waeup/kofa/students/browser.py:439
+msgid "unset"
+msgstr "ungesetzt"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:547
+msgid "${a} <br /> Application Record ${b}"
+msgstr "${a} <br /> Antrag ${b}"
+
+#: waeup/kofa/applicants/browser.py:637 waeup/kofa/students/utils.py:426
+#: waeup/kofa/students/utils.py:459
+msgid "This type of payment has already been made."
+msgstr "Diese Bezahlung wurde bereits ausgeführt."
+
+#: waeup/kofa/applicants/browser.py:649 waeup/kofa/students/browser.py:1655
+#: waeup/kofa/students/browser.py:1693 waeup/kofa/students/browser.py:1726
+msgid "Payment ticket created."
+msgstr "Bezahl-Ticket wurde erzeugt."
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:672 waeup/kofa/students/browser.py:1749
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr "${a}: Online-Bezahl-Ticket ${b}"
+
+#: waeup/kofa/applicants/browser.py:68
+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:717 waeup/kofa/students/browser.py:1806
+msgid "Payment Data"
+msgstr "Zahlung"
+
+#: waeup/kofa/applicants/browser.py:723 waeup/kofa/students/browser.py:1812
+msgid "Online Payment Slip"
+msgstr "Quittung"
+
+#: waeup/kofa/applicants/browser.py:749
+#, 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:770
+msgid "Uploaded image is too big!"
+msgstr "Das hochgeladene Bild ist zu groß!"
+
+#: waeup/kofa/applicants/browser.py:775
+msgid "jpg file extension expected."
+msgstr "jpg-Dateiendung erwartet."
+
+#: waeup/kofa/applicants/browser.py:795 waeup/kofa/applicants/browser.py:952
+#: waeup/kofa/applicants/browser.py:1008
+msgid "Final Submit"
+msgstr "Endgültig einreichen"
+
+#: waeup/kofa/applicants/browser.py:796 waeup/kofa/applicants/browser.py:908
+#: waeup/kofa/applicants/browser.py:950 waeup/kofa/applicants/browser.py:985
+msgid "Add online payment ticket"
+msgstr "Ticket für Online-Bezahlung hinzufügen"
+
+#: waeup/kofa/applicants/browser.py:796 waeup/kofa/applicants/browser.py:913
+#: waeup/kofa/applicants/browser.py:950 waeup/kofa/applicants/browser.py:953
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/students/browser.py:1343
+#: waeup/kofa/students/browser.py:1391 waeup/kofa/students/browser.py:1583
+#: waeup/kofa/students/browser.py:2557
+msgid "Remove selected tickets"
+msgstr "Ausgewählte Tickets löschen"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:826
+msgid "${a} <br /> Application Form ${b}"
+msgstr "${a} <br /> Antragsformular ${b}"
+
+#: waeup/kofa/applicants/browser.py:838 waeup/kofa/students/browser.py:592
+msgid "No transition"
+msgstr "Keine Änderung"
+
+#: waeup/kofa/applicants/browser.py:84
+msgid "Application Section"
+msgstr "Antragstellung"
+
+#: waeup/kofa/applicants/browser.py:883 waeup/kofa/students/browser.py:1589
+msgid "No payment selected."
+msgstr "Nichts ausgewählt!"
+
+#: waeup/kofa/applicants/browser.py:961 waeup/kofa/students/browser.py:108
+msgid "The requested form is locked (read-only)."
+msgstr "Dieses Formular ist gesperrt."
+
+#: waeup/kofa/applicants/browser.py:977
+msgid "No passport picture uploaded."
+msgstr "Es wurde noch kein Passbild hochgeladen."
+
+#: waeup/kofa/applicants/browser.py:979
+msgid "Passport picture confirmation box not ticked."
+msgstr "Bitte bestätigen Sie das Passbild."
+
+#: 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:52
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:28
+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:25
+msgid "Passport Photo:"
+msgstr "Passbild:"
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:50
+msgid "Application Transition:"
+msgstr "Zustandsänderung der Antragstellung:"
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:66
+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:82
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:40
+#, fuzzy
+msgid "Application Fee Payment Tickets"
+msgstr "Zullassungsgebühr-Tickets"
+
+#: 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/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:46
+msgid "Application Number"
+msgstr "Antragsnummer"
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:50
+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:92
+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:93
+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:38
+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:56
+msgid "Records"
+msgstr "Anzahl"
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:6
+msgid "Introduction"
+msgstr "Einleitung"
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:12
+msgid "Application Period"
+msgstr "Beantragungsszeitraum"
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:50
+msgid "Year"
+msgstr "Jahr"
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:11
+msgid "with email address"
+msgstr "mit Emailadresse"
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:5
+msgid "for applicant"
+msgstr "nach Antragsteller"
+
+#: waeup/kofa/applicants/export.py:39
+msgid "Basic Applicants Containers"
+msgstr "Basisordner für Antragsteller"
+
+#: waeup/kofa/applicants/interfaces.py:143
+msgid "Create Application Records"
+msgstr "Anträge hinzufügen"
+
+#: waeup/kofa/applicants/interfaces.py:144
+msgid "Update Application Records"
+msgstr "Anträge aktualisieren"
+
+#: waeup/kofa/applicants/interfaces.py:174
+#: waeup/kofa/applicants/interfaces.py:238
+msgid "Human readable description in HTML format"
+msgstr "Lesbare Beschreibung im HTML-Format"
+
+#: waeup/kofa/applicants/interfaces.py:192 waeup/kofa/students/browser.py:1130
+#: waeup/kofa/students/browser.py:1290 waeup/kofa/university/interfaces.py:38
+#: waeup/kofa/university/interfaces.py:66
+#: waeup/kofa/university/interfaces.py:86
+#: waeup/kofa/university/interfaces.py:117
+#: waeup/kofa/university/interfaces.py:139
+#: waeup/kofa/university/interfaces.py:188
+#: waeup/kofa/university/interfaces.py:201
+#: waeup/kofa/university/interfaces.py:311
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:39
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:68
+#: waeup/kofa/browser/templates/facultypage.pt:8
+#: waeup/kofa/browser/templates/departmentpage.pt:15
+#: waeup/kofa/browser/templates/departmentpage.pt:38
+#: waeup/kofa/browser/templates/searchpage.pt:13
+#: waeup/kofa/browser/templates/facultymanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:15
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:26
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:52
+msgid "Code"
+msgstr "Kürzel"
+
+#: waeup/kofa/applicants/interfaces.py:198 waeup/kofa/students/browser.py:1131
+#: waeup/kofa/students/browser.py:1291 waeup/kofa/students/interfaces.py:597
+#: waeup/kofa/university/interfaces.py:208
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:70
+#: waeup/kofa/browser/templates/facultypage.pt:9
+#: waeup/kofa/browser/templates/departmentpage.pt:16
+#: waeup/kofa/browser/templates/departmentpage.pt:39
+#: waeup/kofa/browser/templates/searchpage.pt:14
+#: waeup/kofa/browser/templates/certificatepage.pt:32
+#: waeup/kofa/browser/templates/facultymanagepage.pt:43
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:38
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:37
+#: waeup/kofa/students/browser_templates/transcript.pt:38
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:16
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:27
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:54
+msgid "Title"
+msgstr "Titel"
+
+#: waeup/kofa/applicants/interfaces.py:204
+#: waeup/kofa/applicants/interfaces.py:318
+msgid "Application Target"
+msgstr "Antragsobjekt (Programm)"
+
+#: waeup/kofa/applicants/interfaces.py:211
+#: waeup/kofa/applicants/interfaces.py:325
+msgid "Year of Entrance"
+msgstr "Eingangsjahr"
+
+#: waeup/kofa/applicants/interfaces.py:218
+msgid "Application Mode"
+msgstr "Modus der Antragstellung"
+
+#: waeup/kofa/applicants/interfaces.py:232
+msgid "Category for the grouping of certificates"
+msgstr "Kategorie für die Gruppierung von Zertifikaten"
+
+#: waeup/kofa/applicants/interfaces.py:251
+msgid "Application Start Date"
+msgstr "Beginn der Antragsperiode"
+
+#: waeup/kofa/applicants/interfaces.py:253
+#: waeup/kofa/applicants/interfaces.py:259 waeup/kofa/hostels/interfaces.py:39
+#: waeup/kofa/hostels/interfaces.py:45
+msgid "Example:"
+msgstr "Beispiel:"
+
+#: waeup/kofa/applicants/interfaces.py:257
+msgid "Application Closing Date"
+msgstr "Ende der Antragsperiode"
+
+#: waeup/kofa/applicants/interfaces.py:263
+msgid "Forbid additions after deadline (enddate)"
+msgstr "Antragstellung nach Ablauf der Antragsfrist verbieten"
+
+#: waeup/kofa/applicants/interfaces.py:269 waeup/kofa/interfaces.py:652
+msgid "Application Fee"
+msgstr "Antragsgebühr"
+
+#: waeup/kofa/applicants/interfaces.py:275
+#, fuzzy
+msgid "Hide container"
+msgstr "Ordner für Antragsteller verwalten"
+
+#: waeup/kofa/applicants/interfaces.py:353
+#: waeup/kofa/students/interfaces.py:185
+msgid "Account suspended"
+msgstr "Konto gesperrt"
+
+#: waeup/kofa/applicants/interfaces.py:359
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:26
+msgid "Applicant Id"
+msgstr "Antragsteller Id"
+
+#: waeup/kofa/applicants/interfaces.py:364
+#: waeup/kofa/students/interfaces.py:225 waeup/kofa/students/interfaces.py:356
+msgid "Registration Number"
+msgstr "Registrierungsnummer"
+
+#: waeup/kofa/applicants/interfaces.py:375
+#: waeup/kofa/applicants/interfaces.py:526
+#: waeup/kofa/students/interfaces.py:204 waeup/kofa/students/interfaces.py:381
+msgid "First Name"
+msgstr "Vorname"
+
+#: waeup/kofa/applicants/interfaces.py:379
+#: waeup/kofa/students/interfaces.py:209
+msgid "Middle Name"
+msgstr "Zweiter Vorname"
+
+#: waeup/kofa/applicants/interfaces.py:383
+#: waeup/kofa/students/interfaces.py:214
+msgid "Last Name (Surname)"
+msgstr "Nachname"
+
+#: waeup/kofa/applicants/interfaces.py:387
+#: waeup/kofa/students/interfaces.py:297
+msgid "Date of Birth"
+msgstr "Geburtsdatum"
+
+#: waeup/kofa/applicants/interfaces.py:393
+#: waeup/kofa/students/interfaces.py:219
+msgid "Sex"
+msgstr "Geschlecht"
+
+#: waeup/kofa/applicants/interfaces.py:398
+#: waeup/kofa/applicants/interfaces.py:474
+#: waeup/kofa/applicants/interfaces.py:531
+#: waeup/kofa/browser/interfaces.py:221 waeup/kofa/interfaces.py:435
+#: waeup/kofa/interfaces.py:511 waeup/kofa/students/interfaces.py:386
+msgid "Email Address"
+msgstr "Emailadresse"
+
+#: waeup/kofa/applicants/interfaces.py:403 waeup/kofa/interfaces.py:440
+#: waeup/kofa/interfaces.py:518 waeup/kofa/students/interfaces.py:250
+msgid "Phone"
+msgstr "Telefon"
+
+#: waeup/kofa/applicants/interfaces.py:408
+#: waeup/kofa/applicants/interfaces.py:479
+msgid "1st Choice Course of Study"
+msgstr "Studienfach (1. Wahl)"
+
+#: waeup/kofa/applicants/interfaces.py:413
+#: waeup/kofa/applicants/interfaces.py:484
+msgid "2nd Choice Course of Study"
+msgstr "Studienfach (2. Wahl)"
+
+#: waeup/kofa/applicants/interfaces.py:425
+#: waeup/kofa/applicants/interfaces.py:495
+msgid "Notice"
+msgstr "Bemerkung"
+
+#: waeup/kofa/applicants/interfaces.py:429
+#: waeup/kofa/students/interfaces.py:199
+#: waeup/kofa/browser/templates/showstudentspage.pt:4
+#: waeup/kofa/students/browser_templates/containerpage.pt:32
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:38
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:56
+msgid "Student Id"
+msgstr "Studenten Id"
+
+#: waeup/kofa/applicants/interfaces.py:434
+#: waeup/kofa/applicants/interfaces.py:489
+msgid "Admitted Course of Study"
+msgstr "Zugelassenes Studienfach"
+
+#: waeup/kofa/applicants/interfaces.py:439
+msgid "Form locked"
+msgstr "Formular gesperrt"
+
+#: waeup/kofa/applicants/payment.py:53 waeup/kofa/applicants/workflow.py:74
+#: waeup/kofa/students/payments.py:115
+msgid "Payment approved"
+msgstr "Zahlung bestätigt"
+
+#: waeup/kofa/applicants/payment.py:66 waeup/kofa/students/payments.py:104
+msgid "Successful payment"
+msgstr "Erfolgreiche Zahlung"
+
+#: waeup/kofa/applicants/payment.py:73 waeup/kofa/students/payments.py:122
+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:40
+msgid "Desired Study Courses"
+msgstr "Gewünschter Studiengang"
+
+#: waeup/kofa/applicants/utils.py:41
+msgid "Process Data"
+msgstr "Prozessdaten"
+
+#: waeup/kofa/applicants/utils.py:56 waeup/kofa/students/browser.py:139
+#: waeup/kofa/students/utils.py:356 waeup/kofa/students/utils.py:383
+#: waeup/kofa/students/utils.py:394 waeup/kofa/students/utils.py:450
+msgid "Session configuration object is not available."
+msgstr "Session-Konfiguration ist nicht verfügbar."
+
+#: 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:146 waeup/kofa/applicants/viewlets.py:155
+#, fuzzy
+msgid "Create students"
+msgstr "Studentendatensatz erzeugen"
+
+#: waeup/kofa/applicants/viewlets.py:163
+msgid "View application record"
+msgstr "Antrag anschauen"
+
+#: waeup/kofa/applicants/viewlets.py:171
+msgid "Manage application record"
+msgstr "Antrag verwalten"
+
+#: waeup/kofa/applicants/viewlets.py:179
+msgid "Edit application record"
+msgstr "Antrag bearbeiten"
+
+#: waeup/kofa/applicants/viewlets.py:197
+msgid "Download application slip"
+msgstr "Antrag herunterladen"
+
+#: waeup/kofa/applicants/viewlets.py:213 waeup/kofa/applicants/workflow.py:108
+#: waeup/kofa/students/browser.py:361
+msgid "Create student record"
+msgstr "Studenten erzeugen"
+
+#: waeup/kofa/applicants/viewlets.py:230 waeup/kofa/students/viewlets.py:480
+msgid "Download payment slip"
+msgstr "Quittung herunterladen"
+
+#: waeup/kofa/applicants/viewlets.py:245 waeup/kofa/applicants/workflow.py:73
+#: waeup/kofa/students/viewlets.py:495 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:728
+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:91
+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:90
+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:93
+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:652
+msgid "Academics"
+msgstr "Akademie"
+
+#: waeup/kofa/browser/breadcrumbs.py:127 waeup/kofa/browser/pages.py:460
+msgid "Administration"
+msgstr "Verwaltung"
+
+#: waeup/kofa/browser/breadcrumbs.py:134 waeup/kofa/browser/viewlets.py:335
+#: waeup/kofa/browser/viewlets.py:703
+msgid "Portal Configuration"
+msgstr "Portalkonfiguration"
+
+#: waeup/kofa/browser/breadcrumbs.py:152 waeup/kofa/browser/pages.py:507
+#: waeup/kofa/browser/viewlets.py:344 waeup/kofa/browser/viewlets.py:713
+msgid "Portal Users"
+msgstr "Benutzer"
+
+#: waeup/kofa/browser/breadcrumbs.py:159 waeup/kofa/browser/pages.py:885
+#: waeup/kofa/browser/pages.py:1493 waeup/kofa/browser/viewlets.py:353
+#: waeup/kofa/browser/viewlets.py:722
+msgid "Data Center"
+msgstr "Datenzentrum"
+
+#: waeup/kofa/browser/breadcrumbs.py:166 waeup/kofa/browser/reports.py:37
+#: waeup/kofa/browser/viewlets.py:362 waeup/kofa/browser/viewlets.py:731
+#, fuzzy
+msgid "Reports"
+msgstr "Reimportieren"
+
+#: waeup/kofa/browser/breadcrumbs.py:174 waeup/kofa/students/browser.py:2910
+#, fuzzy
+msgid "Student Data Exports"
+msgstr "Studiengangdaten"
+
+#: waeup/kofa/browser/breadcrumbs.py:94
+msgid "Home"
+msgstr "Startseite"
+
+#: waeup/kofa/browser/interfaces.py:212
+msgid "Unique Identifier"
+msgstr "Kennung"
+
+#: waeup/kofa/browser/interfaces.py:213
+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:97
+msgid "'Are you sure?'"
+msgstr "'Sind Sie sicher?'"
+
+#: waeup/kofa/browser/pages.py:1009
+msgid "All import managers have been notified by email."
+msgstr "Alle Import-Manager wurden per Email benachrichtigt."
+
+#: waeup/kofa/browser/pages.py:1011 waeup/kofa/browser/pages.py:2385
+#: waeup/kofa/students/browser.py:491
+msgid "An smtp server error occurred."
+msgstr "Es trat ein smtp-Serverfehler auf."
+
+#: waeup/kofa/browser/pages.py:1019
+#, 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:1033
+msgid "Only csv files are allowed."
+msgstr "Es sind nur csv-Dateien erlaubt."
+
+#: waeup/kofa/browser/pages.py:1041
+msgid "File with same name was uploaded earlier."
+msgstr "Eine Datei gleichen Namens wurde bereits hochgeladen."
+
+#: waeup/kofa/browser/pages.py:1052 waeup/kofa/browser/pages.py:1059
+msgid "Your file contains forbidden characters. Please replace."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1169 waeup/kofa/browser/pages.py:1199
+#: waeup/kofa/browser/pages.py:1302 waeup/kofa/browser/pages.py:1415
+msgid "Process CSV file"
+msgstr "Verarbeite CSF-Datei"
+
+#: waeup/kofa/browser/pages.py:1183 waeup/kofa/browser/pages.py:1267
+#: waeup/kofa/browser/pages.py:1391
+msgid "Import aborted."
+msgstr "Import abgebrochen."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:119
+msgid "Local role successfully removed: ${a}"
+msgstr "Lokale Rolle erfolgreich entfernt: ${a}"
+
+#: waeup/kofa/browser/pages.py:1202
+msgid "Back to step 1"
+msgstr "Zurück zu Schritt 1"
+
+#: waeup/kofa/browser/pages.py:1203
+msgid "Proceed to step 3"
+msgstr "Gehe zu Schritt 3"
+
+#: waeup/kofa/browser/pages.py:1246
+msgid "Double headers: each column name may only appear once. "
+msgstr "Doppelte Spaltenüberschrift"
+
+#: waeup/kofa/browser/pages.py:1254
+msgid "Replace imported file!"
+msgstr "Tauschen Sie die Importdatei aus!"
+
+#: waeup/kofa/browser/pages.py:1279
+msgid "Update mode only!"
+msgstr "Nur Aktualisierungsmodus!"
+
+#: waeup/kofa/browser/pages.py:1305 waeup/kofa/browser/pages.py:1534
+msgid "Reset"
+msgstr "Zurücksetzen"
+
+#: waeup/kofa/browser/pages.py:1306
+msgid "Set headerfields"
+msgstr "Spaltenüberschrift setzen"
+
+#: waeup/kofa/browser/pages.py:1307
+msgid "Back to step 2"
+msgstr "Zurück zu Schritt 2"
+
+#: waeup/kofa/browser/pages.py:1308
+msgid "Perform import"
+msgstr "Import durchführen"
+
+#: waeup/kofa/browser/pages.py:132 waeup/kofa/browser/pages.py:899
+#: waeup/kofa/hostels/browser.py:290 waeup/kofa/hostels/browser.py:320
+#: waeup/kofa/students/browser.py:333
+msgid "No item selected."
+msgstr "Nichts ausgewählt."
+
+#: waeup/kofa/browser/pages.py:1373
+msgid "Edit headers or replace imported file!"
+msgstr ""
+"Bearbeiten Sie die Spaltenüberschrift oder tauschen Sie die Importdatei aus."
+
+#: waeup/kofa/browser/pages.py:1417
+#, fuzzy
+msgid "Process next"
+msgstr "Prozessorname"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1467
+msgid "Processing of ${a} rows failed."
+msgstr "Bearbeitung von ${a} Zeilen fehlgeschlagen."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1469
+msgid "Successfully processed ${a} rows."
+msgstr "${a} Zeilen erfolgreich verarbeitet."
+
+#: waeup/kofa/browser/pages.py:1477
+msgid "Show logfiles"
+msgstr "Logdateien anzeigen"
+
+#: waeup/kofa/browser/pages.py:1479
+msgid "Back to Data Center"
+msgstr "Zurück zum Datenzentrum"
+
+#: waeup/kofa/browser/pages.py:1480
+msgid "Show"
+msgstr "Zeigen"
+
+#: waeup/kofa/browser/pages.py:1496
+msgid "Back"
+msgstr "Zurück"
+
+#: waeup/kofa/browser/pages.py:1497
+msgid "Enter a regular expression here..."
+msgstr "Geben Sie einen regulären Ausdruck ein ..."
+
+#: waeup/kofa/browser/pages.py:1505
+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:1520
+msgid "Invalid search expression."
+msgstr "Ungültiger Suchausdruck"
+
+#: waeup/kofa/browser/pages.py:1523
+msgid "No search results found."
+msgstr "Keinen Datensatz gefunden."
+
+#: waeup/kofa/browser/pages.py:1531
+msgid "Edit data center settings"
+msgstr "Einstellungen des Datenzentrums bearbeiten"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1551
+msgid "File already existed (not copied): ${a}"
+msgstr "Datei existiert (nicht kopiert): ${a}"
+
+#: waeup/kofa/browser/pages.py:1554
+msgid "Given storage path cannot be used."
+msgstr "Der Datenpfad kann nicht verwendet werden."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1555
+msgid "Error: ${a}"
+msgstr "Fehler: ${a}"
+
+#: waeup/kofa/browser/pages.py:1558
+msgid "New storage path succefully set."
+msgstr "Neuen Datenpfad erfolgreich gesetzt."
+
+#: waeup/kofa/browser/pages.py:1570
+msgid "Download portal data as CSV file"
+msgstr "Portaldaten als CSV-Datei herunterladen"
+
+#: waeup/kofa/browser/pages.py:1572
+#, fuzzy
+msgid "Create CSV file"
+msgstr "CSV-Datei exportieren"
+
+#: waeup/kofa/browser/pages.py:1597 waeup/kofa/students/browser.py:2923
+#, fuzzy
+msgid "Discarded export"
+msgstr "Verworfene Export-Ergebnisse"
+
+#: waeup/kofa/browser/pages.py:1689
+msgid "Academic Section"
+msgstr "Akademie"
+
+#: waeup/kofa/browser/pages.py:1701 waeup/kofa/browser/pages.py:1720
+#: waeup/kofa/browser/pages.py:1737 waeup/kofa/browser/pages.py:1741
+msgid "Add faculty"
+msgstr "Fakultät hinzufügen"
+
+#: waeup/kofa/browser/pages.py:1702 waeup/kofa/university/export.py:36
+msgid "Faculties"
+msgstr "Fakultäten"
+
+#: waeup/kofa/browser/pages.py:1706 waeup/kofa/browser/viewlets.py:419
+msgid "Manage academic section"
+msgstr "Akademie verwalten"
+
+#: waeup/kofa/browser/pages.py:1715
+#, fuzzy
+msgid "You are not allowed to remove entire faculties."
+msgstr "Ihnen ist nicht erlaubt, Betttickets zu entfernen."
+
+#: waeup/kofa/browser/pages.py:1748
+msgid "The faculty code chosen already exists."
+msgstr "Das Fakultätskürzel existiert bereits."
+
+#: waeup/kofa/browser/pages.py:1773 waeup/kofa/browser/pages.py:1783
+#: waeup/kofa/university/export.py:78
+msgid "Departments"
+msgstr "Departments"
+
+#: waeup/kofa/browser/pages.py:1785 waeup/kofa/browser/pages.py:1833
+#: waeup/kofa/browser/pages.py:1852 waeup/kofa/browser/pages.py:1856
+msgid "Add department"
+msgstr "Department hinzufügen"
+
+#: waeup/kofa/browser/pages.py:1793 waeup/kofa/browser/viewlets.py:446
+msgid "Manage faculty"
+msgstr "Fakultät verwalten"
+
+#: waeup/kofa/browser/pages.py:1819
+#, fuzzy
+msgid "You are not allowed to remove entire departments."
+msgstr "Ihnen ist nicht erlaubt, Betttickets zu entfernen."
+
+#: waeup/kofa/browser/pages.py:1863
+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:1866
+msgid "Department ${a} added."
+msgstr "Department ${a} hinzugefügt."
+
+#: waeup/kofa/browser/pages.py:1888
+msgid "Courses and Certificates"
+msgstr "Kurse und Zertifikate"
+
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1971
+msgid "Remove selected courses"
+msgstr "Ausgewählte Kurse löschen"
+
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1986
+#: waeup/kofa/browser/pages.py:2015 waeup/kofa/browser/pages.py:2019
+msgid "Add course"
+msgstr "Kurs hinzufügen"
+
+#: waeup/kofa/browser/pages.py:1920 waeup/kofa/browser/pages.py:1977
+msgid "Remove selected certificates"
+msgstr "Ausgewählte Zertifikate löschen"
+
+#: waeup/kofa/browser/pages.py:1920 waeup/kofa/browser/pages.py:1991
+#: waeup/kofa/browser/pages.py:2056 waeup/kofa/browser/pages.py:2060
+msgid "Add certificate"
+msgstr "Zertifikat hinzufügen"
+
+#: waeup/kofa/browser/pages.py:1929 waeup/kofa/browser/viewlets.py:453
+msgid "Manage department"
+msgstr "Department verwalten"
+
+#: waeup/kofa/browser/pages.py:1980
+#, fuzzy
+msgid "You are not allowed to remove certificates."
+msgstr "Ihnen ist nicht erlaubt, Betttickets zu entfernen."
+
+#: waeup/kofa/browser/pages.py:2031
+msgid "A course with same code already exists: "
+msgstr "Ein Kurs mit gleichem Kürzel existiert bereits:"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2037
+msgid "Course ${a} successfully created."
+msgstr "Kurs ${a} erfolgreich erzeugt."
+
+#: waeup/kofa/browser/pages.py:2072
+msgid "A certificate with same code already exists: "
+msgstr "Ein Zertifikat mit gleichem Kürzel existiert bereits:"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2078
+msgid "Certificate ${a} successfully created."
+msgstr "Zertifikat ${a} erfolgreich erzeugt."
+
+#: waeup/kofa/browser/pages.py:2113 waeup/kofa/browser/viewlets.py:483
+msgid "Edit course"
+msgstr "Kurs bearbeiten"
+
+#: waeup/kofa/browser/pages.py:2155
+msgid "Edit certificate"
+msgstr "Zertifikat bearbeiten"
+
+#: waeup/kofa/browser/pages.py:2162 waeup/kofa/browser/pages.py:2191
+#: waeup/kofa/browser/pages.py:2228 waeup/kofa/browser/pages.py:2230
+msgid "Add certificate course"
+msgstr "Zertifikat-Kurs hinzufügen"
+
+#: waeup/kofa/browser/pages.py:2163 waeup/kofa/browser/pages.py:2186
+msgid "Remove selected certificate courses"
+msgstr "Ausgewählte Zertifikat-Kurse löschen"
+
+#: waeup/kofa/browser/pages.py:2169 waeup/kofa/browser/viewlets.py:513
+msgid "Manage certificate"
+msgstr "Zertifikat verwalten"
+
+#: waeup/kofa/browser/pages.py:2235
+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:2239
+msgid "certificate course ${a}_${b} added."
+msgstr "Zertifikat-Kurs ${a}_${b} hinzugefügt."
+
+#: waeup/kofa/browser/pages.py:2280 waeup/kofa/browser/viewlets.py:543
+msgid "Edit certificate course"
+msgstr "Zertifikat-Kurs bearbeiten"
+
+#: waeup/kofa/browser/pages.py:2283
+msgid "Save and return"
+msgstr "Speichen und zurück"
+
+#: waeup/kofa/browser/pages.py:2316
+msgid "Send me a new password"
+msgstr "Neues Passwort zusenden"
+
+#: waeup/kofa/browser/pages.py:2364
+msgid "No record found."
+msgstr "Keinen Datensatz gefunden."
+
+#: waeup/kofa/browser/pages.py:2378 waeup/kofa/students/browser.py:2748
+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:2382
+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:246 waeup/kofa/browser/viewlets.py:281
+#: waeup/kofa/browser/templates/staffsitelayout.pt:28
+#: waeup/kofa/browser/templates/studentsitelayout.pt:32
+msgid "Login"
+msgstr "Einloggen"
+
+#: waeup/kofa/browser/pages.py:257
+msgid "You logged in."
+msgstr "Sie sind jetzt eingeloggt."
+
+#: waeup/kofa/browser/pages.py:266
+msgid "Your personal data record is outdated. Please update."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:300
+#, fuzzy
+msgid "Your account has been temporarily deactivated."
+msgstr "Kurseliste wurde validiert."
+
+#: waeup/kofa/browser/pages.py:307
+#, fuzzy
+msgid "Your account has been deactivated."
+msgstr "Kurseliste wurde validiert."
+
+#: waeup/kofa/browser/pages.py:309
+#, fuzzy
+msgid "You entered invalid credentials."
+msgstr "Sie haben falsche Zugangsdaten eingegeben."
+
+#: waeup/kofa/browser/pages.py:324
+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:365
+msgid "Contact ${a}"
+msgstr "Kontaktiere ${a}"
+
+#: waeup/kofa/browser/pages.py:371 waeup/kofa/browser/pages.py:652
+msgid "Send message now"
+msgstr "Jetzt senden"
+
+#: waeup/kofa/browser/pages.py:390 waeup/kofa/browser/pages.py:423
+#: waeup/kofa/browser/pages.py:669 waeup/kofa/students/browser.py:489
+msgid "Your message has been sent."
+msgstr "Ihre Nachricht wurde verschickt."
+
+#: waeup/kofa/browser/pages.py:410
+msgid "Send now"
+msgstr "Sende jetzt"
+
+#: waeup/kofa/browser/pages.py:425
+msgid "A smtp server error occurred."
+msgstr "Es trat ein smtp-Serverfehler auf."
+
+#: waeup/kofa/browser/pages.py:450
+msgid "<h1>Welcome to WAeUP.Kofa</h1>"
+msgstr "<h1>Willkommen bei WAeUP.Kofa</h1>"
+
+#: waeup/kofa/browser/pages.py:508 waeup/kofa/hostels/browser.py:228
+#: waeup/kofa/students/viewlets.py:168 waeup/kofa/students/viewlets.py:252
+#: waeup/kofa/students/viewlets.py:321 waeup/kofa/students/viewlets.py:337
+#: waeup/kofa/students/viewlets.py:396 waeup/kofa/students/viewlets.py:463
+msgid "Manage"
+msgstr "Verwalten"
+
+#: waeup/kofa/browser/pages.py:509 waeup/kofa/browser/viewlets.py:190
+msgid "Remove"
+msgstr "Entfernen"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:517
+msgid "User account ${a} successfully deleted."
+msgstr "Benutzerkonto ${a} erfolgreich gelöscht."
+
+#: waeup/kofa/browser/pages.py:554 waeup/kofa/browser/pages.py:556
+#: waeup/kofa/browser/viewlets.py:549
+msgid "Add user"
+msgstr "Benutzer hinzufügen"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:578
+msgid "User account ${a} successfully added."
+msgstr "Benutzerkonto ${a} erfolgreich hinzugefügt."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:599
+msgid "Edit user ${a}"
+msgstr "Benutzer bearbeiten"
+
+#: waeup/kofa/browser/pages.py:633
+msgid "User settings have been saved."
+msgstr "Benutzereinstellungen wurden gespeichert."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:650 waeup/kofa/students/browser.py:470
+msgid "Send message to ${a}"
+msgstr "Sende Nachricht an ${a}"
+
+#: waeup/kofa/browser/pages.py:679 waeup/kofa/browser/viewlets.py:369
+msgid "My Preferences"
+msgstr "Meine Präferenzen"
+
+#: waeup/kofa/browser/pages.py:692 waeup/kofa/browser/viewlets.py:385
+msgid "My Roles"
+msgstr "Meine Rollen"
+
+#: waeup/kofa/browser/pages.py:723
+msgid "Search Academic Section"
+msgstr "Akademie durchsuchen"
+
+#: waeup/kofa/browser/pages.py:750
+msgid "View portal configuration"
+msgstr "Portal-Konfiguration betrachten"
+
+#: waeup/kofa/browser/pages.py:763
+msgid "Edit portal configuration"
+msgstr "Portalkonfiguration bearbeiten"
+
+#: waeup/kofa/browser/pages.py:764 waeup/kofa/browser/pages.py:814
+msgid "Update plugins"
+msgstr "Plugins aktualisieren"
+
+#: waeup/kofa/browser/pages.py:766 waeup/kofa/browser/pages.py:793
+#: waeup/kofa/browser/pages.py:826
+msgid "Add session configuration"
+msgstr "Session-Konfiguration hinzufügen"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:805
+msgid "Session ${a} Configuration"
+msgstr "Session ${a}-Konfiguration"
+
+#: waeup/kofa/browser/pages.py:817
+msgid "Plugins were updated. See log file for details."
+msgstr "Plugins wurden aktualisiert, siehe Logdatei."
+
+#: waeup/kofa/browser/pages.py:830
+msgid "Add Session Configuration"
+msgstr "Session-Konfiguration hinzufügen"
+
+#: waeup/kofa/browser/pages.py:841
+msgid "The session chosen already exists."
+msgstr "Die Session existiert bereits."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:863
+msgid "Edit academic session ${a} configuration"
+msgstr "Session ${a}-Konfiguration bearbeiten"
+
+#: waeup/kofa/browser/pages.py:910
+msgid "OSError: The file could not be deleted."
+msgstr "OSError: Die Datei konnte nicht gelöscht werden."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:913
+msgid "Successfully deleted: ${a}"
+msgstr "Successfully deleted: ${a}"
+
+#: waeup/kofa/browser/pages.py:924
+#, fuzzy
+msgid "Processed Files"
+msgstr "Verarbeite CSF-Datei"
+
+#: waeup/kofa/browser/pages.py:935
+#, fuzzy
+msgid "Upload portal data as CSV file"
+msgstr "Portaldaten als CSV-Datei herunterladen"
+
+#: waeup/kofa/browser/pages.py:938
+msgid "Upload"
+msgstr "Hochladen"
+
+#: waeup/kofa/browser/pages.py:984
+#, fuzzy
+msgid "Import Manager"
+msgstr "Import abgebrochen."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:986
+#, fuzzy
+msgid "${a}: ${b} uploaded"
+msgstr "Datei ${a} hochgeladen."
+
+#: waeup/kofa/browser/pages.py:99
+msgid "No local role selected."
+msgstr "Keine lokalen Rollen ausgewählt."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:990
+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/pdf.py:153
+#, fuzzy
+msgid "Date:"
+msgstr "Datum"
+
+#: waeup/kofa/browser/pdf.py:204 waeup/kofa/browser/pdf.py:461
+#: waeup/kofa/browser/pdf.py:471
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:12
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:16
+msgid "Date"
+msgstr "Datum"
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:522
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr "${footer_text} Seite ${num1} of ${num2}"
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:791
+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
+#: waeup/kofa/students/browser_templates/exportconfig_course.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:6
+msgid "Certificate Courses"
+msgstr "Zertifikat-Kurs"
+
+#: waeup/kofa/browser/templates/certificatepage.pt:29
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:35
+#: waeup/kofa/students/browser_templates/transcript.pt:36
+msgid "Semester"
+msgstr "Semester"
+
+#: waeup/kofa/browser/templates/certificatepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:36
+msgid "Cert. Course"
+msgstr "Cert.-Kurs"
+
+#: waeup/kofa/browser/templates/certificatepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:37
+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:8
+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:34
+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/datacenterexportpage.pt:44
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:4
+#, fuzzy
+msgid "Export Number"
+msgstr "Nummer"
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:45
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:5
+#, fuzzy
+msgid "Exporter"
+msgstr "Exportiere Daten"
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:46
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:6
+msgid "Filter"
+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 "Schritt 1"
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr "Dateiname"
+
+#: 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:16
+msgid "Sample Record 1"
+msgstr "Beispielsdatensatz 1"
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 2"
+msgstr "Beispielsdatensatz 2"
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:18
+msgid "Sample Record 3"
+msgstr "Beispielsdatensatz 3"
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:41
+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:24
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:13
+msgid "Processing mode:"
+msgstr "Verarbeitungsmodus:"
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:37
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:15
+msgid "Header"
+msgstr "Überschrift"
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:38
+msgid "change to"
+msgstr "ändern zu:"
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:39
+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:2
+msgid "Batch processing finished."
+msgstr "Stapelverarbeitung beendet."
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:14
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr "Dateigröße"
+
+#: 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/datacenterlogspage.pt:36
+#, fuzzy
+msgid "Reload page"
+msgstr "Seite neu laden"
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:12
+msgid "Overwrite contents in new location?"
+msgstr "Existierende Dateien an neuem Ort überschreiben?"
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:3
+#: waeup/kofa/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr "Pfad des Speicherorts:"
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:7
+msgid "Copy existing data to new storage?"
+msgstr "Existierende Dateien an neuen Ort kopieren?"
+
+#: 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:18
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:14
+msgid "Datasets"
+msgstr "Datensätze"
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:19
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:11
+msgid "Upload Date"
+msgstr "Hochladedatum"
+
+#: 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:113
+msgid "Download"
+msgstr "Herunterladen"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr "Datei:"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:22
+msgid "Processor:"
+msgstr "Prozessor:"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:35
+#, fuzzy
+msgid "Import Mode:"
+msgstr "Import abgebrochen."
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:59
+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:67
+msgid "Available Processors (Importers)"
+msgstr "Zur Verfügung Prozessoren"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:72
+#, fuzzy
+msgid "Processor"
+msgstr "Prozessor:"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:73
+msgid "Required Schema Fields"
+msgstr "Erforderliche Schema-Felder"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:74
+msgid "Optional Schema Fields"
+msgstr "Optionale Schema-Felder"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:75
+msgid "Non-Schema Fields"
+msgstr "Nicht-Schema-Felder"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:76
+msgid "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:101
+#: waeup/kofa/browser/templates/facultymanagepage.pt:75
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:97
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:112
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:91
+msgid "Local Role"
+msgstr "Lokale Rolle"
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:16
+#: waeup/kofa/browser/templates/userscontainerpage.pt:7
+#: waeup/kofa/browser/templates/facultymanagepage.pt:16
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:17
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:16
+msgid "Local Roles"
+msgstr "Lokale Rollen"
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:6
+#: waeup/kofa/browser/templates/facultymanagepage.pt:6
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:5
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:7
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:9
+msgid "Settings"
+msgstr "Einstellungen"
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:99
+#: waeup/kofa/browser/templates/userscontainerpage.pt:5
+#: waeup/kofa/browser/templates/facultymanagepage.pt:73
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:110
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:89
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:22
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:39
+msgid "Name"
+msgstr "Name"
+
+#: waeup/kofa/browser/templates/facultypage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr "Es existieren bisher keine Unterobjekte."
+
+#: waeup/kofa/browser/templates/fileupload.pt:26
+#: waeup/kofa/students/browser_templates/imageupload.pt:20
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:32
+msgid "Max. file size:"
+msgstr "Max. Dateigröße:"
+
+#: waeup/kofa/browser/templates/loginpage.pt:13
+#: waeup/kofa/browser/templates/usereditformpage.pt:9
+#: waeup/kofa/students/browser_templates/basepage.pt:14
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:18
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:27
+#: waeup/kofa/students/browser_templates/change_password.pt:10
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:31
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:27
+msgid "Password:"
+msgstr "Passwort:"
+
+#: waeup/kofa/browser/templates/loginpage.pt:24
+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:36
+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:39
+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:44
+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:48
+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:5
+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/reportscontainercreate.pt:22
+#, fuzzy
+msgid "Type of report to create"
+msgstr "Zu exportierender Objekttyp"
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:4
+#, fuzzy
+msgid "Report Number"
+msgstr "Registrierungsnummer"
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:41
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:76
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:36
+msgid "<img src=\"${DYNAMIC_CONTENT}\" /> Reload"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:5
+#, fuzzy
+msgid "Description"
+msgstr "Beschreibung/Nachricht"
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:51
+#, fuzzy
+msgid "Create new report"
+msgstr "Studenten erzeugen"
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:6
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:47
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:7
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:15
+msgid "Creator"
+msgstr "Erzeuger"
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:7
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:48
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:91
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:47
+msgid "Creation Date"
+msgstr "Erstellungsdatum"
+
+#: waeup/kofa/browser/templates/searchpage.pt:16
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:17
+msgid "Department"
+msgstr "Department"
+
+#: waeup/kofa/browser/templates/searchpage.pt:17
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:46
+msgid "Type"
+msgstr "Typ"
+
+#: waeup/kofa/browser/templates/searchpage.pt:9
+#: waeup/kofa/students/browser_templates/containerpage.pt:24
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:18
+msgid "Search Results"
+msgstr "Suchergebnisse"
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:10
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:62
+msgid "Comment"
+msgstr ""
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:5
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:57
+msgid "Fullname"
+msgstr "Vollständiger Name"
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:6
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:8
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:49
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:58
+msgid "Status"
+msgstr "Status"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:113
+#: waeup/kofa/browser/templates/studentsitelayout.pt:97
+msgid "Back to bottom"
+msgstr "Zurück zum Ende"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:122
+#: waeup/kofa/browser/templates/studentsitelayout.pt:107
+msgid "Back to top"
+msgstr "Zurück zum Anfang"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:124
+#: waeup/kofa/browser/templates/studentsitelayout.pt:109
+msgid "Kofa - Student Management System"
+msgstr "Kofa - Student Management System"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:127
+#: waeup/kofa/browser/templates/studentsitelayout.pt:112
+#, fuzzy
+msgid "Copyright &copy; WAeUP Group 2008-2013"
+msgstr "Copyright &copy; WAeUP Group 2008-2012"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:34
+#: waeup/kofa/browser/templates/studentsitelayout.pt:38
+msgid "Contact"
+msgstr "Kontakt"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:4
+#: waeup/kofa/browser/templates/studentsitelayout.pt:4
+msgid "WAeUP.Kofa - Student Management System"
+msgstr "WAeUP.Kofa - Student Management System"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:40
+#: waeup/kofa/browser/templates/studentsitelayout.pt:44
+msgid "Logout"
+msgstr "Ausloggen"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:62
+msgid "Student Id:"
+msgstr "Studenten Id:"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:66
+msgid "State:"
+msgstr "Status:"
+
+#: waeup/kofa/browser/templates/usereditformpage.pt:15
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:24
+#: waeup/kofa/students/browser_templates/change_password.pt:16
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:44
+msgid "Retype Password:"
+msgstr "Wiederhole Passwort:"
+
+#: waeup/kofa/browser/viewlets.py:180
+msgid "Add"
+msgstr "Hinzufügen"
+
+#: waeup/kofa/browser/viewlets.py:320
+msgid "Any link text"
+msgstr "Any link text"
+
+#: waeup/kofa/browser/viewlets.py:402 waeup/kofa/students/viewlets.py:160
+msgid "Send email"
+msgstr "Email versenden"
+
+#: waeup/kofa/browser/viewlets.py:411
+msgid "Edit settings"
+msgstr "Einstellungen bearbeiten"
+
+#: waeup/kofa/browser/viewlets.py:428 waeup/kofa/browser/viewlets.py:474
+#: waeup/kofa/browser/viewlets.py:504 waeup/kofa/browser/viewlets.py:534
+#, fuzzy
+msgid "Export student data"
+msgstr "Exportiere Daten"
+
+#: waeup/kofa/browser/viewlets.py:437
+msgid "Search academic section"
+msgstr "Akademie durchsuchen"
+
+#: waeup/kofa/browser/viewlets.py:463 waeup/kofa/browser/viewlets.py:493
+#: waeup/kofa/browser/viewlets.py:523
+msgid "Show students"
+msgstr "Studenten zeigen"
+
+#: waeup/kofa/browser/viewlets.py:558
+msgid "Show logs"
+msgstr "Logdateien anzeigen"
+
+#: waeup/kofa/browser/viewlets.py:567
+#, fuzzy
+msgid "Upload data"
+msgstr "Hochladedatum"
+
+#: waeup/kofa/browser/viewlets.py:576
+#, fuzzy
+msgid "Process data"
+msgstr "Prozessdaten"
+
+#: waeup/kofa/browser/viewlets.py:585
+msgid "Export data"
+msgstr "Exportiere Daten"
+
+#: waeup/kofa/browser/viewlets.py:594
+#, fuzzy
+msgid "View processed files"
+msgstr "Logdatei betrachten"
+
+#: waeup/kofa/browser/viewlets.py:667
+msgid "Enquiries"
+msgstr "Anfragen"
+
+#: waeup/kofa/browser/viewlets.py:95 waeup/kofa/browser/viewlets.py:170
+#: waeup/kofa/students/viewlets.py:329 waeup/kofa/students/viewlets.py:536
+#: waeup/kofa/students/viewlets.py:583
+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:163
+#: waeup/kofa/hostels/browser.py:190
+msgid "Add hostel"
+msgstr "Wohnheim hinzufügen"
+
+#: waeup/kofa/hostels/browser.py:133 waeup/kofa/hostels/browser.py:168
+#, fuzzy
+msgid "Clear all hostels"
+msgstr "Wohnheim erzeugen"
+
+#: waeup/kofa/hostels/browser.py:171
+#, fuzzy
+msgid "All hostels cleared."
+msgstr "Wohnheim erzeugt."
+
+#: waeup/kofa/hostels/browser.py:179
+msgid "Settings have been saved."
+msgstr "Einstellungen wurden gespeichert."
+
+#: waeup/kofa/hostels/browser.py:193
+msgid "Create hostel"
+msgstr "Wohnheim erzeugen"
+
+#: waeup/kofa/hostels/browser.py:202
+msgid "The hostel already exists."
+msgstr "Das Wohnheim existiert bereits."
+
+#: waeup/kofa/hostels/browser.py:204
+msgid "Hostel created."
+msgstr "Wohnheim erzeugt."
+
+#: waeup/kofa/hostels/browser.py:239
+msgid "Manage hostel"
+msgstr "Wohnheim verwalten"
+
+#: waeup/kofa/hostels/browser.py:242 waeup/kofa/hostels/browser.py:270
+msgid "Update all beds"
+msgstr "Alle Betten aktualisieren"
+
+#: waeup/kofa/hostels/browser.py:243 waeup/kofa/hostels/browser.py:284
+msgid "Switch reservation of selected beds"
+msgstr "Reservierung der ausgewählten Betten ändern"
+
+#: waeup/kofa/hostels/browser.py:244 waeup/kofa/hostels/browser.py:314
+msgid "Release selected beds"
+msgstr "Ausgewählte Betten freigeben"
+
+#: waeup/kofa/hostels/browser.py:245 waeup/kofa/hostels/browser.py:341
+#, fuzzy
+msgid "Clear hostel"
+msgstr "Wohnheim erzeugen"
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:275
+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:308
+msgid "Successfully switched beds: ${a}"
+msgstr "Erfolgreich geänderte Betten: ${a}"
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:332
+msgid "Successfully released beds: ${a}"
+msgstr "Erfolgreich freigegebene Betten: ${a}"
+
+#: waeup/kofa/hostels/browser.py:337
+msgid "No allocated bed selected."
+msgstr "Keine Betten ausgewählt."
+
+#: waeup/kofa/hostels/browser.py:344
+#, fuzzy
+msgid "Hostel cleared."
+msgstr "Wohnheim erzeugt."
+
+#: waeup/kofa/hostels/browser.py:357
+msgid "Allocate student"
+msgstr "Student zuweisen"
+
+#: waeup/kofa/hostels/browser.py:363
+#, 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/containerpage.pt:21
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:37
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:45
+msgid "Id"
+msgstr "Id"
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:23
+#, fuzzy
+msgid "Booked Beds"
+msgstr "Bett B"
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:47
+msgid "Number"
+msgstr "Nummer"
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:6
+msgid "Hostel Data"
+msgstr "Wohnheimdaten"
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:66
+msgid "[allocate student]"
+msgstr "[Student zuweisen]"
+
+#: waeup/kofa/hostels/browser_templates/hostelpage.pt:5
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:19
+#, fuzzy
+msgid "Reserved Beds:"
+msgstr "Reservierte Betten"
+
+#: waeup/kofa/hostels/export.py:61
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:9
+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:105
+msgid "Floors per Block"
+msgstr "Etagen pro Block"
+
+#: waeup/kofa/hostels/interfaces.py:111
+msgid "Rooms per Floor"
+msgstr "Räume pro Flur"
+
+#: waeup/kofa/hostels/interfaces.py:117
+msgid "Reserved Beds"
+msgstr "Reservierte Betten"
+
+#: waeup/kofa/hostels/interfaces.py:128
+msgid "Blocks for Female Students"
+msgstr "Blöcke für Frauen"
+
+#: waeup/kofa/hostels/interfaces.py:135
+msgid "Blocks for Male Students"
+msgstr "Blöcke für Männer"
+
+#: waeup/kofa/hostels/interfaces.py:142
+#, fuzzy
+msgid "Beds for Pre-Study Students"
+msgstr "Betten für Erstsemester"
+
+#: waeup/kofa/hostels/interfaces.py:149
+msgid "Beds for Fresh Students"
+msgstr "Betten für Erstsemester"
+
+#: waeup/kofa/hostels/interfaces.py:156
+msgid "Beds for Returning Students"
+msgstr "Betten für rückgemeldete Studenten"
+
+#: waeup/kofa/hostels/interfaces.py:163
+msgid "Beds for Final Year Students"
+msgstr "Betten für Studenten im letzten Studienjahr"
+
+#: waeup/kofa/hostels/interfaces.py:170
+msgid "Beds without category"
+msgstr "Unkategorisierte Betten"
+
+#: waeup/kofa/hostels/interfaces.py:177
+msgid "Special Handling"
+msgstr "Spezielle Handhabung"
+
+#: waeup/kofa/hostels/interfaces.py:188
+msgid "Female and male blocks overlap."
+msgstr "Blöcke für Männer und Frauen überlappen."
+
+#: waeup/kofa/hostels/interfaces.py:198
+msgid "Bed categories overlap."
+msgstr "Bettkategorien überlappen."
+
+#: waeup/kofa/hostels/interfaces.py:220
+msgid "Bed Id"
+msgstr "Betten Id"
+
+#: waeup/kofa/hostels/interfaces.py:226
+msgid "Bed Type"
+msgstr "Betttyp"
+
+#: waeup/kofa/hostels/interfaces.py:232
+msgid "Bed Number"
+msgstr "Bettnummer"
+
+#: waeup/kofa/hostels/interfaces.py:237
+msgid "Owner (Student)"
+msgstr "Eigentümer (Student)"
+
+#: waeup/kofa/hostels/interfaces.py:238
+#, fuzzy
+msgid "Enter valid student id."
+msgstr "Sie haben falsche Zugangsdaten eingegeben."
+
+#: waeup/kofa/hostels/interfaces.py:253
+#, 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:260
+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:93
+msgid "Sort Id"
+msgstr "Sortierungsnummer"
+
+#: waeup/kofa/hostels/interfaces.py:99
+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
+msgid "Block K"
+msgstr "Block K"
+
+#: waeup/kofa/hostels/vocabularies.py:63
+msgid "Block L"
+msgstr "Block L"
+
+#: waeup/kofa/hostels/vocabularies.py:64
+msgid "Block M"
+msgstr "Block M"
+
+#: waeup/kofa/hostels/vocabularies.py:65
+msgid "Block N"
+msgstr "Block N"
+
+#: waeup/kofa/hostels/vocabularies.py:66
+msgid "Block O"
+msgstr "Block O"
+
+#: waeup/kofa/hostels/vocabularies.py:67
+msgid "Block P"
+msgstr "Block P"
+
+#: waeup/kofa/hostels/vocabularies.py:68
+#, fuzzy
+msgid "Block Q"
+msgstr "Block A"
+
+#: waeup/kofa/hostels/vocabularies.py:69
+#, fuzzy
+msgid "Block R"
+msgstr "Block A"
+
+#: waeup/kofa/hostels/vocabularies.py:70
+#, fuzzy
+msgid "Block S"
+msgstr "Block A"
+
+#: waeup/kofa/hostels/vocabularies.py:71
+#, fuzzy
+msgid "Block T"
+msgstr "Block A"
+
+#: waeup/kofa/hostels/vocabularies.py:72
+#, fuzzy
+msgid "Block U"
+msgstr "Block A"
+
+#: waeup/kofa/hostels/vocabularies.py:73
+#, fuzzy
+msgid "Block V"
+msgstr "Block A"
+
+#: waeup/kofa/hostels/vocabularies.py:74
+#, fuzzy
+msgid "Block W"
+msgstr "Block A"
+
+#: waeup/kofa/interfaces.py:207
+msgid "Subject"
+msgstr "Fach"
+
+#: waeup/kofa/interfaces.py:211 waeup/kofa/students/browser.py:1134
+#: waeup/kofa/students/browser.py:1297
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:10
+msgid "Grade"
+msgstr "Note"
+
+#: waeup/kofa/interfaces.py:368
+msgid "Processor name"
+msgstr "Prozessorname"
+
+#: waeup/kofa/interfaces.py:394
+msgid "Email Address:"
+msgstr "Emailadresse:"
+
+#: waeup/kofa/interfaces.py:401
+msgid "Email to:"
+msgstr "Email an:"
+
+#: waeup/kofa/interfaces.py:408
+msgid "Subject:"
+msgstr "Betreff:"
+
+#: waeup/kofa/interfaces.py:412
+msgid "Full Name:"
+msgstr "Vollständiger Name:"
+
+#: waeup/kofa/interfaces.py:416
+msgid "Text:"
+msgstr "Text:"
+
+#: waeup/kofa/interfaces.py:445 waeup/kofa/interfaces.py:501
+msgid "Public Name"
+msgstr "Öffentlicher Name"
+
+#: waeup/kofa/interfaces.py:455 waeup/kofa/interfaces.py:456
+#, fuzzy
+msgid "Number of failed logins"
+msgstr "Zugangscode-Nummer"
+
+#: waeup/kofa/interfaces.py:462
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:463
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:492
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:97
+#: waeup/kofa/browser/templates/userscontainerpage.pt:4
+#: waeup/kofa/browser/templates/facultymanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:93
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:108
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:87
+msgid "User Id"
+msgstr "Benutzer Id"
+
+#: waeup/kofa/interfaces.py:497
+#: waeup/kofa/students/browser_templates/containerpage.pt:36
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:42
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:48
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:29
+msgid "Full Name"
+msgstr "Vollständiger Name"
+
+#: waeup/kofa/interfaces.py:507
+msgid "Description/Notice"
+msgstr "Beschreibung/Nachricht"
+
+#: waeup/kofa/interfaces.py:524
+#: waeup/kofa/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr "Portalweite Rollen:"
+
+#: waeup/kofa/interfaces.py:571
+msgid "Name of University"
+msgstr "Name der Universität"
+
+#: waeup/kofa/interfaces.py:572
+msgid "Sample University"
+msgstr "Deomo-Universität"
+
+#: waeup/kofa/interfaces.py:577
+msgid "Abbreviated Title of University"
+msgstr "Abkürzung"
+
+#: waeup/kofa/interfaces.py:583
+msgid "Skin"
+msgstr "Skin"
+
+#: waeup/kofa/interfaces.py:59
+msgid "new"
+msgstr "neu"
+
+#: waeup/kofa/interfaces.py:590
+msgid "Content in HTML format"
+msgstr "Inhalt im HTML-Format"
+
+#: waeup/kofa/interfaces.py:60
+msgid "completed"
+msgstr "abgeschlossen"
+
+#: waeup/kofa/interfaces.py:602
+msgid "Name of Administrator"
+msgstr "Name des Administrators"
+
+#: waeup/kofa/interfaces.py:608
+msgid "Email Address of Administrator"
+msgstr "Emailadresse des Administrators"
+
+#: waeup/kofa/interfaces.py:61
+msgid "pending"
+msgstr "unerledigt"
+
+#: waeup/kofa/interfaces.py:615
+msgid "Subject of Email to Administrator"
+msgstr "Betreff der Email an Administrator"
+
+#: waeup/kofa/interfaces.py:616
+msgid "Kofa Contact"
+msgstr "Kofa-Kontakt"
+
+#: waeup/kofa/interfaces.py:62
+msgid "active"
+msgstr "aktiv"
+
+#: waeup/kofa/interfaces.py:621
+msgid "SMTP mailer to use when sending mail"
+msgstr "SMTP Dienst"
+
+#: waeup/kofa/interfaces.py:628
+msgid "Captcha used for public registration pages"
+msgstr "Captcha für öffentliche Registrierung"
+
+#: waeup/kofa/interfaces.py:63
+msgid "assigned"
+msgstr "zugewiesen"
+
+#: waeup/kofa/interfaces.py:635
+msgid "Carry-over Course Registration"
+msgstr "Carry-Over-Kursregistrierung"
+
+#: waeup/kofa/interfaces.py:64
+#, fuzzy
+msgid "callbacks"
+msgstr "Bitte um Rückruf"
+
+#: waeup/kofa/interfaces.py:644 waeup/kofa/interfaces.py:690
+msgid "Academic Session"
+msgstr "Akademische Session"
+
+#: waeup/kofa/interfaces.py:658
+#, fuzzy
+msgid "Acceptance Fee"
+msgstr "Acceptance Letter"
+
+#: waeup/kofa/interfaces.py:664
+msgid "Bed Booking Fee"
+msgstr "Betten-Buchungsgebühr"
+
+#: waeup/kofa/interfaces.py:670
+msgid "Rent"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:676
+#, fuzzy
+msgid "Clearance enabled"
+msgstr "Einschreibung gestartet"
+
+#: waeup/kofa/interfaces.py:92
+msgid "clearance started"
+msgstr "Einschreibung begonnen"
+
+#: waeup/kofa/interfaces.py:93
+msgid "clearance requested"
+msgstr "Einschreibung beantragt"
+
+#: waeup/kofa/interfaces.py:94
+msgid "cleared"
+msgstr "eingeschrieben"
+
+#: waeup/kofa/interfaces.py:95
+msgid "school fee paid"
+msgstr "Gebühren bezahlt"
+
+#: waeup/kofa/interfaces.py:96
+msgid "courses registered"
+msgstr "Kurse registriert"
+
+#: waeup/kofa/interfaces.py:97
+msgid "courses validated"
+msgstr "Kurse validiert"
+
+#: waeup/kofa/interfaces.py:98
+msgid "returning"
+msgstr "rückgemeldet"
+
+#: 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
+msgid ""
+"Password has been successfully set. Proceed to the login page and enter your "
+"credentials."
+msgstr ""
+"Das Passwort wurde erfolgreich gesetzt. Bitte gehen Sie zu Login-Seite."
+
+#: waeup/kofa/objecthistory.py:74
+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:782
+#: waeup/kofa/students/interfaces.py:806
+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:789
+#: waeup/kofa/students/interfaces.py:813
+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/applicants/browser_templates/applicanteditpage.pt:92
+#: 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:853
+msgid "System"
+msgstr "System"
+
+#: waeup/kofa/students/browser.py:1024
+msgid "You must select a session for the level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1033
+msgid "Study level has been added."
+msgstr "Level wurde hinzugefügt."
+
+#: waeup/kofa/students/browser.py:1035 waeup/kofa/students/browser.py:2477
+msgid "This level exists."
+msgstr "Dieses Level existiert."
+
+#: waeup/kofa/students/browser.py:1045
+msgid "No study level selected."
+msgstr "Kein Level ausgewählt."
+
+#: waeup/kofa/students/browser.py:1073
+#, fuzzy
+msgid "You are not allowed to view the transcript."
+msgstr "Ihnen ist nicht erlaubt, Betttickets zu entfernen."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1088
+#, fuzzy
+msgid "${a}: Transcript Data"
+msgstr "${a}: Einschreibungsdaten"
+
+#: waeup/kofa/students/browser.py:1106
+#, fuzzy
+msgid "You are not allowed to download the transcript."
+msgstr "Ihnen ist nicht erlaubt, Betttickets zu entfernen."
+
+#: waeup/kofa/students/browser.py:1121 waeup/kofa/students/viewlets.py:366
+#, fuzzy
+msgid "Academic Transcript"
+msgstr "Akademie"
+
+#: waeup/kofa/students/browser.py:1125
+msgid "CERTIFIED TRUE COPY"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1129 waeup/kofa/students/browser.py:1289
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:35
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:25
+msgid "Sem."
+msgstr "Sem."
+
+#: waeup/kofa/students/browser.py:1132 waeup/kofa/students/interfaces.py:623
+#: waeup/kofa/university/interfaces.py:152
+#: 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:1133 waeup/kofa/students/browser.py:1296
+#: waeup/kofa/students/interfaces.py:634 waeup/kofa/students/interfaces.py:667
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:20
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:32
+msgid "Score"
+msgstr "Score"
+
+#: waeup/kofa/students/browser.py:1161 waeup/kofa/students/viewlets.py:203
+#, fuzzy
+msgid "Transfer student"
+msgstr "Studentendatensatz erzeugen"
+
+#: waeup/kofa/students/browser.py:1171
+msgid "Transfer"
+msgstr "Studienfachwechsel"
+
+#: waeup/kofa/students/browser.py:1175
+#, fuzzy
+msgid "Current level does not match certificate levels."
+msgstr "Das Studienjaht ist "
+
+#: waeup/kofa/students/browser.py:1177
+#, fuzzy
+msgid "Former study course record incomplete."
+msgstr "Die Studiengangdaten sind unvollständig."
+
+#: waeup/kofa/students/browser.py:1179
+#, fuzzy
+msgid "Maximum number of transfers exceeded."
+msgstr "Maximale Anzahl der Credit-Punkte von ${a} überschritten."
+
+#: waeup/kofa/students/browser.py:1181
+#, fuzzy
+msgid "Successfully transferred."
+msgstr "Erfolgreich hinzugefügt: ${a}"
+
+#: waeup/kofa/students/browser.py:1191
+msgid "Revert previous transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1196
+msgid "No previous transfer."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1201
+#, fuzzy
+msgid "Revert now"
+msgstr "Jetzt beginnen"
+
+#: waeup/kofa/students/browser.py:1204
+msgid "Previous transfer reverted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1235
+msgid "${a}: Study Level ${b}"
+msgstr "${a}: Level ${b}"
+
+#: waeup/kofa/students/browser.py:1256
+msgid "Level Data"
+msgstr "Leveldaten"
+
+#: waeup/kofa/students/browser.py:1262
+#, fuzzy
+msgid "1st Semester Courses"
+msgstr "Kurse registrieren"
+
+#: waeup/kofa/students/browser.py:1268
+#, fuzzy
+msgid "2nd Semester Courses"
+msgstr "Kurse registrieren"
+
+#: waeup/kofa/students/browser.py:1274
+#, fuzzy
+msgid "Level Courses"
+msgstr "Level-Kürzel"
+
+#: waeup/kofa/students/browser.py:1283
+msgid "Course Registration Slip"
+msgstr "Kursregistrierungsbeleg"
+
+#: waeup/kofa/students/browser.py:1292
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:38
+msgid "Dept."
+msgstr "Dept."
+
+#: waeup/kofa/students/browser.py:1293
+#: waeup/kofa/browser/templates/searchpage.pt:15
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:22
+msgid "Faculty"
+msgstr "Fakultät"
+
+#: waeup/kofa/students/browser.py:1294
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:40
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:30
+msgid "Cred."
+msgstr "Cred."
+
+#: waeup/kofa/students/browser.py:1342 waeup/kofa/students/browser.py:1481
+#: waeup/kofa/students/browser.py:1492 waeup/kofa/students/browser.py:2602
+msgid "Add course ticket"
+msgstr "Kursticket hinzufügen"
+
+#: waeup/kofa/students/browser.py:1360 waeup/kofa/students/browser.py:2507
+msgid "No valid course code entered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1366 waeup/kofa/students/browser.py:2512
+#, fuzzy
+msgid "Course not found."
+msgstr "Keinen Studenten gefunden."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1383
+msgid "Manage study level ${a}"
+msgstr "Verwalte Level  ${a}"
+
+#: waeup/kofa/students/browser.py:1397 waeup/kofa/students/browser.py:2536
+msgid "No ticket selected."
+msgstr "Kein Ticket ausgewählt."
+
+#: waeup/kofa/students/browser.py:141
+msgid "Clearance is disabled for this session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1427 waeup/kofa/students/browser.py:1452
+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:1431
+msgid "Course list has been validated."
+msgstr "Kurseliste wurde validiert."
+
+#: waeup/kofa/students/browser.py:1433 waeup/kofa/students/browser.py:1464
+msgid "Student is in the wrong state."
+msgstr "Student hat den falschen Status."
+
+#: waeup/kofa/students/browser.py:1457
+msgid "Course list request has been annulled."
+msgstr "Kursregistrierung wurde annulliert."
+
+#: waeup/kofa/students/browser.py:1461
+msgid "Course list request has been rejected:"
+msgstr "Kursregistrierung wurde abgelehnt."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1516
+msgid "${a}: Course Ticket ${b}"
+msgstr "${a}: Kursticket ${b}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:152
+#, fuzzy
+msgid "Total credits exceed ${a}."
+msgstr "Gesamte Credits"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1541
+msgid "Manage course ticket ${a}"
+msgstr "Verwalte Kursticket ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1574
+msgid "${a}: Payments"
+msgstr "${a}: Zahlungen"
+
+#: waeup/kofa/students/browser.py:159
+msgid "The ticket exists."
+msgstr "Das Ticket existiert."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:161
+msgid "Successfully added ${a}."
+msgstr "Erfolgreich hinzugefügt: ${a}"
+
+#: waeup/kofa/students/browser.py:1622
+msgid "Add online payment"
+msgstr "Ticket für Online-Bezahlung hinzufügen"
+
+#: waeup/kofa/students/browser.py:1630 waeup/kofa/students/browser.py:1680
+#: waeup/kofa/students/browser.py:1711
+msgid "Create ticket"
+msgstr "Ticket erzeugen"
+
+#: waeup/kofa/students/browser.py:1640 waeup/kofa/students/browser.py:1931
+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:1646
+#, fuzzy
+msgid "You have not yet booked accommodation."
+msgstr "Unterkunft buchen"
+
+#: waeup/kofa/students/browser.py:1670
+#, fuzzy
+msgid "Add previous session online payment"
+msgstr "Ticket für Online-Bezahlung hinzufügen"
+
+#: waeup/kofa/students/browser.py:1675
+msgid "No previous payment to be made."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1708
+#, fuzzy
+msgid "Add balance"
+msgstr "Antragsteller hinzufügen"
+
+#: waeup/kofa/students/browser.py:182 waeup/kofa/students/export.py:197
+#: waeup/kofa/students/reports.py:154 waeup/kofa/students/viewlets.py:688
+msgid "Students"
+msgstr "Studenten"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1844
+msgid "${a}: Accommodation"
+msgstr " ${a}: Unterkunft"
+
+#: waeup/kofa/students/browser.py:1856
+msgid "You are not allowed to remove bed tickets."
+msgstr "Ihnen ist nicht erlaubt, Betttickets zu entfernen."
+
+#: waeup/kofa/students/browser.py:1863
+msgid "No bed ticket selected."
+msgstr "Kein Ticket ausgewählt."
+
+#: waeup/kofa/students/browser.py:1895
+msgid "Add bed ticket"
+msgstr "Bettticket hinzufügen"
+
+#: waeup/kofa/students/browser.py:1897
+msgid "Create bed ticket"
+msgstr "Bettticket erzeugen"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1914
+msgid "Outside booking period: ${a} - ${b}"
+msgstr "Außerhalb der Antragsperiode: ${a} - ${b}"
+
+#: waeup/kofa/students/browser.py:1917
+msgid "Outside booking period."
+msgstr "Außerhalb der Antragsperiode."
+
+#: waeup/kofa/students/browser.py:1921
+msgid "Your data are incomplete."
+msgstr "Die Daten sind unvollständig."
+
+#: waeup/kofa/students/browser.py:1925
+msgid "You are in the wrong registration state."
+msgstr "Sie haben den falschen Status."
+
+#: waeup/kofa/students/browser.py:1936
+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:1949 waeup/kofa/students/browser.py:2268
+#: waeup/kofa/students/browser.py:2410
+msgid "Activation code is invalid."
+msgstr "Aktivierungs-Code ist ungültig."
+
+#: waeup/kofa/students/browser.py:1961 waeup/kofa/students/browser.py:1976
+msgid "System error: Please contact the adminsitrator."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1981 waeup/kofa/students/browser.py:2117
+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:1987 waeup/kofa/students/browser.py:2271
+#: waeup/kofa/students/browser.py:2414
+msgid "Activation code has already been used."
+msgstr "Der Aktivierungs-Code wurde bereits verwendet."
+
+#: waeup/kofa/students/browser.py:1990 waeup/kofa/students/browser.py:2275
+#: waeup/kofa/students/browser.py:2417 waeup/kofa/students/browser.py:2662
+msgid "invalidated"
+msgstr "entwertet"
+
+#: waeup/kofa/students/browser.py:1995 waeup/kofa/students/browser.py:2279
+#: waeup/kofa/students/browser.py:2422
+msgid "You are not the owner of this access code."
+msgstr "Sie sind nicht der Eigentümer dieses Zugangscodes."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2007 waeup/kofa/students/browser.py:2131
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr "${a}, Block ${b}, Raum ${c}, Bett ${d} (${e})"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2016
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr "Bettticket erzeugt und Bett gebucht: ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2033
+msgid "Bed Ticket for Session ${a}"
+msgstr "Bett-Ticket für Session ${a}"
+
+#: waeup/kofa/students/browser.py:2052
+msgid "Bed Allocation Data"
+msgstr "Bettenzuweisungsdaten"
+
+#: waeup/kofa/students/browser.py:206 waeup/kofa/students/viewlets.py:119
+#: waeup/kofa/students/viewlets.py:751
+msgid "Study Course"
+msgstr "Studiengang"
+
+#: waeup/kofa/students/browser.py:2061
+#, fuzzy
+msgid "Bed Allocation Slip"
+msgstr "Bettenzuweisung"
+
+#: waeup/kofa/students/browser.py:208
+#, fuzzy
+msgid "Previous Study Course"
+msgstr "Studiengang"
+
+#: waeup/kofa/students/browser.py:2089
+msgid "Students in reserved beds can't be relocated."
+msgstr "Studenten in reservierten Betten können nicht umgezogen werden."
+
+#: waeup/kofa/students/browser.py:2095
+msgid "Student can't be relocated."
+msgstr "Studen konnte nicht umgezogen werden."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2139
+msgid "Student relocated: ${a}"
+msgstr "Student umgezogen: ${a}"
+
+#: waeup/kofa/students/browser.py:214 waeup/kofa/students/viewlets.py:125
+#: waeup/kofa/students/viewlets.py:752
+msgid "Payments"
+msgstr "Zahlungen"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2158
+msgid "${a}: History"
+msgstr "${a}: Historie"
+
+#: waeup/kofa/students/browser.py:2170
+msgid "Edit base data"
+msgstr "Basisdaten editieren"
+
+#: waeup/kofa/students/browser.py:2185 waeup/kofa/students/viewlets.py:545
+msgid "Change password"
+msgstr "Passwort ändern"
+
+#: waeup/kofa/students/browser.py:2199
+msgid "Password changed."
+msgstr "Passwort geändert"
+
+#: waeup/kofa/students/browser.py:2211
+msgid "Upload portrait"
+msgstr "Portrait hochladen"
+
+#: waeup/kofa/students/browser.py:2226 waeup/kofa/students/viewlets.py:569
+#: waeup/kofa/students/workflow.py:48
+msgid "Start clearance"
+msgstr "Einschreibung starten"
+
+#: waeup/kofa/students/browser.py:2230
+msgid "Start clearance now"
+msgstr "Einschreibung jetzt starten"
+
+#: waeup/kofa/students/browser.py:2248
+msgid "Wrong state"
+msgstr "Falscher Status"
+
+#: waeup/kofa/students/browser.py:2252
+msgid "No portrait uploaded."
+msgstr "Es wurde kein Passbild hochgeladen."
+
+#: waeup/kofa/students/browser.py:2256
+msgid "Not all required fields filled."
+msgstr "Es wurden nicht alle Pflichtfelder ausgefüllt."
+
+#: waeup/kofa/students/browser.py:2283
+msgid "Clearance process has been started."
+msgstr "Einschreibung wurde gestartet"
+
+#: waeup/kofa/students/browser.py:229 waeup/kofa/students/viewlets.py:131
+msgid "Accommodation"
+msgstr "Unterkunft"
+
+#: waeup/kofa/students/browser.py:2293
+msgid "Edit clearance data"
+msgstr "Bearbeite Einschreibungsdaten"
+
+#: waeup/kofa/students/browser.py:2314 waeup/kofa/students/browser.py:2328
+msgid "Clearance form has been saved."
+msgstr "Einschreibungsdaten wurden gespeichert."
+
+#: waeup/kofa/students/browser.py:2322
+msgid "Save and request clearance"
+msgstr "Speichern und Einschreibung beantragen"
+
+#: waeup/kofa/students/browser.py:2339 waeup/kofa/students/browser.py:2373
+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:2342 waeup/kofa/students/browser.py:2376
+msgid "Clearance has been requested."
+msgstr "Einschreibung wurde beantragt."
+
+#: waeup/kofa/students/browser.py:2351 waeup/kofa/students/workflow.py:62
+msgid "Request clearance"
+msgstr "Einschreibung beantragen"
+
+#: waeup/kofa/students/browser.py:2352
+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:2355
+msgid "Request clearance now"
+msgstr "Einschreibung beantragen"
+
+#: waeup/kofa/students/browser.py:2367
+msgid "This isn't your CLR access code."
+msgstr "Dies ist nicht Ihr CLR-Zugangscode"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:238
+msgid "Bed Ticket ${a}"
+msgstr "Bett-Ticket ${a}"
+
+#: waeup/kofa/students/browser.py:2385
+msgid "Start session"
+msgstr "Beginne Studienjahr"
+
+#: waeup/kofa/students/browser.py:2389
+msgid "Start now"
+msgstr "Jetzt beginnen"
+
+#: waeup/kofa/students/browser.py:2398
+msgid "You are not entitled to start session."
+msgstr "Sie dürfen das Studienjahr noch nicht beginnen."
+
+#: waeup/kofa/students/browser.py:2435
+msgid "An error occurred, please contact the system administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2437
+msgid "Session started."
+msgstr "Studienjahr begonnen."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2456
+msgid "Add current level ${a}"
+msgstr "Aktuelles Level ${a} hinzufügen"
+
+#: waeup/kofa/students/browser.py:2468
+msgid "Create course list now"
+msgstr "Kursliste jetzt erzeugen"
+
+#: waeup/kofa/students/browser.py:2479
+#, fuzzy
+msgid "Your data are incomplete"
+msgstr "Die Daten sind unvollständig."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2524
+msgid "Edit course list of ${a}"
+msgstr "${a} Kursliste bearbeiten"
+
+#: waeup/kofa/students/browser.py:2565
+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."
+
+#: waeup/kofa/students/browser.py:257
+msgid "Student Section"
+msgstr "Studenten"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2573
+msgid "Maximum credits of ${a} exceeded."
+msgstr "Maximale Anzahl der Credit-Punkte von ${a} überschritten."
+
+#: waeup/kofa/students/browser.py:2578
+msgid "Course list has been registered."
+msgstr "Kurseliste wurde registriert."
+
+#: waeup/kofa/students/browser.py:2582
+msgid "Register course list"
+msgstr "Registriere Kursliste"
+
+#: waeup/kofa/students/browser.py:2618
+msgid "Set password for first-time login"
+msgstr "Passwort setzen"
+
+#: waeup/kofa/students/browser.py:2621
+msgid "Set"
+msgstr "Setzen"
+
+#: waeup/kofa/students/browser.py:2636
+msgid "More than one student found."
+msgstr "Mehr als einen Studenten gefunden."
+
+#: waeup/kofa/students/browser.py:2644
+msgid "Access code is invalid."
+msgstr "Aktivierungs-Code ist ungültig."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2647
+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:2653
+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:2659
+msgid "Access code has already been used."
+msgstr "Der Aktivierungs-Code wurde bereits verwendet."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2668
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr "Passwort wurde gesetzt. Ihre Studenten Id ist ${a}"
+
+#: waeup/kofa/students/browser.py:2680
+msgid "Request password for first-time login"
+msgstr "Passwort zum erstmaligen Einloggen beantragen"
+
+#: waeup/kofa/students/browser.py:2723 waeup/kofa/students/browser.py:2734
+msgid "No student record found."
+msgstr "Keinen Studenten gefunden."
+
+#: waeup/kofa/students/browser.py:2768
+msgid "Your password request was successful."
+msgstr "Ihre Passwortanfrage war erfolgreich."
+
+#: waeup/kofa/students/browser.py:2784 waeup/kofa/students/interfaces.py:417
+#: waeup/kofa/students/interfaces.py:508
+#: waeup/kofa/browser/templates/showstudentspage.pt:8
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:60
+msgid "Current Session"
+msgstr "Aktuelle Session"
+
+#: waeup/kofa/students/browser.py:2785 waeup/kofa/students/interfaces.py:424
+#: waeup/kofa/students/interfaces.py:456 waeup/kofa/students/interfaces.py:514
+#: waeup/kofa/browser/templates/showstudentspage.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:61
+msgid "Current Level"
+msgstr "Aktuelles Level"
+
+#: waeup/kofa/students/browser.py:282
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr "Nur Jafreszahlen erlaubt (z.B. 2011)"
+
+#: waeup/kofa/students/browser.py:2836
+#, fuzzy
+msgid "in any session"
+msgstr "in der Session"
+
+#: waeup/kofa/students/browser.py:2842
+#, fuzzy
+msgid "at any level"
+msgstr "Einstiegslevel"
+
+#: waeup/kofa/students/browser.py:287 waeup/kofa/students/browser.py:331
+#: waeup/kofa/students/browser.py:2633 waeup/kofa/students/browser.py:2845
+msgid "No student found."
+msgstr "Keinen Studenten gefunden."
+
+#: waeup/kofa/students/browser.py:2878 waeup/kofa/students/interfaces.py:539
+#: waeup/kofa/students/interfaces.py:714 waeup/kofa/students/utils.py:224
+#: waeup/kofa/students/browser_templates/transcript.pt:35
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:8
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:14
+msgid "Session"
+msgstr "Session"
+
+#: waeup/kofa/students/browser.py:2879 waeup/kofa/students/utils.py:226
+#: waeup/kofa/university/interfaces.py:340
+#: waeup/kofa/browser/templates/certificatepage.pt:28
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:34
+#: waeup/kofa/students/browser_templates/transcript.pt:34
+msgid "Level"
+msgstr "Level"
+
+#: waeup/kofa/students/browser.py:2936
+#, fuzzy
+msgid "Configure student data export"
+msgstr "Studenten erzeugen"
+
+#: waeup/kofa/students/browser.py:2944
+#, fuzzy
+msgid "All Sessions"
+msgstr "Session"
+
+#: waeup/kofa/students/browser.py:2951
+#, fuzzy
+msgid "All Levels"
+msgstr "Level"
+
+#: waeup/kofa/students/browser.py:2958
+msgid "All Modes"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:298 waeup/kofa/students/viewlets.py:144
+msgid "Manage student section"
+msgstr "Studenten verwalten"
+
+#: waeup/kofa/students/browser.py:3022
+msgid "Export started for students with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3113
+msgid "Export started for course tickets with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:358 waeup/kofa/students/viewlets.py:151
+msgid "Add student"
+msgstr "Student hinzufügen"
+
+#: waeup/kofa/students/browser.py:366
+msgid "Student record created."
+msgstr "Student erzeugt."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:380
+msgid "Set temporary password for ${a}"
+msgstr "Setze temporäres Passwort für ${a}"
+
+#: waeup/kofa/students/browser.py:402
+msgid "Login now"
+msgstr "Jetzt einloggen"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:406
+#, fuzzy
+msgid "Login as ${a}"
+msgstr "Kontaktiere ${a}"
+
+#: waeup/kofa/students/browser.py:412
+#, 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:430
+msgid "${a}: Base Data (account deactivated)"
+msgstr "${a}: Basisdaten (Konto gesperrt)"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:432
+msgid "${a}: Base Data"
+msgstr "${a}: Basisdaten"
+
+#: waeup/kofa/students/browser.py:509
+msgid "Admission Letter of"
+msgstr "Admission Letter von"
+
+#: waeup/kofa/students/browser.py:527
+msgid "Manage base data"
+msgstr "Basisdaten verwalten"
+
+#: waeup/kofa/students/browser.py:576
+#, fuzzy
+msgid "Trigger registration transition"
+msgstr "Zustandsänderung der Registrierung:"
+
+#: waeup/kofa/students/browser.py:616
+#, fuzzy
+msgid "Student account has been activated."
+msgstr "Student wurde eingeschrieben."
+
+#: waeup/kofa/students/browser.py:635
+#, fuzzy
+msgid "Student account has been deactivated."
+msgstr "Student wurde eingeschrieben."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:670
+msgid "${a}: Clearance Data"
+msgstr "${a}: Einschreibungsdaten"
+
+#: waeup/kofa/students/browser.py:700 waeup/kofa/students/viewlets.py:109
+#: waeup/kofa/students/viewlets.py:749
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:6
+msgid "Clearance Data"
+msgstr "Einschreibungsdaten"
+
+#: waeup/kofa/students/browser.py:706
+#, fuzzy
+msgid "Clearance Slip of"
+msgstr "Einschreibungsbeleg für"
+
+#: waeup/kofa/students/browser.py:715
+#, fuzzy
+msgid "Student Signature"
+msgstr "Studenten"
+
+#: waeup/kofa/students/browser.py:716
+#, fuzzy
+msgid "Clearance Officer Signature"
+msgstr "Einschreibungsgebühr"
+
+#: waeup/kofa/students/browser.py:723
+#, fuzzy
+msgid "Date, Student Signature"
+msgstr "Studenten"
+
+#: waeup/kofa/students/browser.py:724
+#, fuzzy
+msgid "Date, Clearance Officer Signature"
+msgstr "Einschreibungsgebühr"
+
+#: waeup/kofa/students/browser.py:745
+msgid "Manage clearance data"
+msgstr "Verwalte Einschreibungsdaten"
+
+#: waeup/kofa/students/browser.py:789
+msgid "Student has been cleared."
+msgstr "Student wurde eingeschrieben."
+
+#: waeup/kofa/students/browser.py:791 waeup/kofa/students/browser.py:826
+msgid "Student is in wrong state."
+msgstr "Student hat den falschen Status."
+
+#: waeup/kofa/students/browser.py:803 waeup/kofa/students/viewlets.py:277
+msgid "Reject clearance"
+msgstr "Einschreibung zurückweisen"
+
+#: waeup/kofa/students/browser.py:815
+#, fuzzy
+msgid "Save comment and reject clearance now"
+msgstr "Speichern und Einschreibung beantragen"
+
+#: waeup/kofa/students/browser.py:819
+msgid "Clearance has been annulled."
+msgstr "Einschreibung wurde annulliert."
+
+#: waeup/kofa/students/browser.py:823
+msgid "Clearance request has been rejected."
+msgstr "Einschreibung wurde abgelehnt."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:856
+msgid "${a}: Personal Data"
+msgstr "${a}: Persönliche Daten"
+
+#: waeup/kofa/students/browser.py:869
+msgid "Manage personal data"
+msgstr "Persönliche Daten verwalten"
+
+#: waeup/kofa/students/browser.py:884
+msgid "Edit personal data"
+msgstr "Persönliche Daten bearbeiten"
+
+#: waeup/kofa/students/browser.py:887
+msgid "Save/Confirm"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:914
+msgid "${a}: Study Course"
+msgstr "${a}: Studiengang"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:917
+#, fuzzy
+msgid "${a}: Previous Study Course"
+msgstr "${a}: Studiengang"
+
+#: waeup/kofa/students/browser.py:945
+#, fuzzy
+msgid "First Study Course, "
+msgstr "Studiengang"
+
+#: waeup/kofa/students/browser.py:947
+#, fuzzy
+msgid "Second Study Course"
+msgstr "Studiengangdaten"
+
+#: waeup/kofa/students/browser.py:952
+#, fuzzy
+msgid "First Study Course"
+msgstr "Studiengang"
+
+#: waeup/kofa/students/browser.py:963
+msgid "Manage study course"
+msgstr "Studiengang verwalten"
+
+#: waeup/kofa/students/browser.py:966 waeup/kofa/students/browser.py:1039
+msgid "Remove selected levels"
+msgstr "Ausgewählte Levels  löschen"
+
+#: waeup/kofa/students/browser.py:967 waeup/kofa/students/browser.py:1019
+msgid "Add study level"
+msgstr "Level  hinzufügen"
+
+#: waeup/kofa/students/browser.py:999
+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/clearanceeditpage.pt:8
+msgid "Scans"
+msgstr "Scans"
+
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:31
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:38
+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/applicants/browser_templates/applicantssearchpage.pt:10
+msgid "with registration number"
+msgstr "mit Regsistrierungsnummer"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:11
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:11
+msgid "with matriculation number"
+msgstr "mit Matrikelnummer"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:12
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:12
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:78
+msgid "in session"
+msgstr "in der Session"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:13
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:13
+msgid "in department"
+msgstr "im Department"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:14
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:14
+msgid "in study mode"
+msgstr "im Studienmodus"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:15
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:15
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:33
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:27
+msgid "Reg. Number"
+msgstr "Registrierungsnummer"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:34
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:40
+msgid "Matric. Number"
+msgstr "Matrikelnummer"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:35
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:41
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:15
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:25
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "State"
+msgstr "Status"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:5
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:5
+msgid "for students"
+msgstr "nach Studenten"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:8
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:8
+msgid "with id"
+msgstr "mit Id"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:9
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:9
+msgid "with name"
+msgstr "mit Name"
+
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:18
+msgid "Weight"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:20
+msgid "The activation code can be found on the payment slip."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:34
+msgid ""
+"Your data have been approved. You can continue by clicking the button below."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr "Aktivierungs-Code:"
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:48
+#, fuzzy
+msgid "Current Study Mode:"
+msgstr "Studienmodus:"
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig.pt:1
+msgid ""
+"<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."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:12
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:2
+#: waeup/kofa/students/browser_templates/exportconfig.pt:12
+#, fuzzy
+msgid "Exporter (Data Type):"
+msgstr "Exportiere Daten"
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:24
+#: waeup/kofa/students/browser_templates/exportconfig.pt:24
+#, fuzzy
+msgid "Current Session:"
+msgstr "Aktuelle Session"
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:36
+#: waeup/kofa/students/browser_templates/exportconfig.pt:36
+#, fuzzy
+msgid "Current Level:"
+msgstr "Aktuelles Level"
+
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:14
+#, fuzzy
+msgid "Level Session:"
+msgstr "Session"
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:50
+#, 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:93
+#: 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:94
+#: 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:90
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:46
+msgid "Payment Id"
+msgstr "Zahlungs-Id"
+
+#: 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: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:32
+msgid "Instructions:"
+msgstr "Anleitung:"
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:33
+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/studentsreportgeneratorpage.pt:10
+#, fuzzy
+msgid "Study Modes Group:"
+msgstr "Studienmodus:"
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.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/browser_templates/studentsreportgeneratorpage.pt:21
+#, fuzzy
+msgid "Set study modes filter."
+msgstr "im Studienmodus"
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.pt:33
+msgid "<option value=\"${DYNAMIC_CONTENT}\"> ${DYNAMIC_CONTENT} </option>"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.pt:37
+#, fuzzy
+msgid "Select academic session to report."
+msgstr "Akademie durchsuchen"
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:6
+msgid "Study Course Data"
+msgstr "Studiengangdaten"
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:9
+msgid "Study Levels"
+msgstr "Levels"
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr "Studienmodus:"
+
+#: 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
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:30
+msgid "Study Levels (Course Lists)"
+msgstr "Levels (Kurslisten)"
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:69
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:35
+msgid "Level Code"
+msgstr "Level-Kürzel"
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:70
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:36
+msgid "Level Title"
+msgstr "Level-Titel"
+
+#: 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: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:39
+msgid "Fact."
+msgstr "Fkt."
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:31
+msgid "Requ."
+msgstr "Erford."
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:43
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:21
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:34
+msgid "CO"
+msgstr "CO"
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:44
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:35
+msgid "Auto"
+msgstr "Auto"
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:6
+msgid "Study Level Data"
+msgstr "Level-Daten"
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:87
+#: 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/studylevelmanagepage.pt:93
+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:28
+msgid "Dep."
+msgstr "Dep."
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:29
+msgid "Fac."
+msgstr "Fak."
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:58
+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/export.py:220
+msgid "Student Study Courses"
+msgstr "Studiengangdaten"
+
+#: waeup/kofa/students/export.py:250
+msgid "Student Study Levels"
+msgstr "Levels"
+
+#: waeup/kofa/students/export.py:275
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:9
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:30
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:20
+msgid "Course Tickets"
+msgstr "Kurstickets"
+
+#: waeup/kofa/students/export.py:309
+msgid "Student Payments"
+msgstr "Student Bezahlungen"
+
+#: waeup/kofa/students/export.py:346
+#, fuzzy
+msgid "Payment Data for Bursary"
+msgstr "Zahlung"
+
+#: waeup/kofa/students/export.py:377
+#, fuzzy
+msgid "Bed Tickets"
+msgstr "Bett-Ticket ${a}"
+
+#: waeup/kofa/students/export.py:413
+#, fuzzy
+msgid "Student Payments Overview"
+msgstr "Student Bezahlungen"
+
+#: waeup/kofa/students/export.py:442
+#, fuzzy
+msgid "Student Study Levels Overview"
+msgstr "Levels"
+
+#: waeup/kofa/students/export.py:471
+msgid "Combo Card Data"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:191
+#, fuzzy
+msgid "Reasons for Deactivation"
+msgstr "Response-Beschreibung"
+
+#: waeup/kofa/students/interfaces.py:193
+msgid ""
+"This message will be shown if and only if deactivated students try to login."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:232 waeup/kofa/students/interfaces.py:365
+msgid "Matriculation Number"
+msgstr "Matrikelnummer"
+
+#: waeup/kofa/students/interfaces.py:239
+msgid "PWD Activation Code"
+msgstr "PWD-Aktivierungs-Code"
+
+#: waeup/kofa/students/interfaces.py:245
+msgid "Email"
+msgstr "Emailadresse"
+
+#: waeup/kofa/students/interfaces.py:280
+msgid "Officer's Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:285
+msgid "Clearance form locked"
+msgstr "Einschreibungsformular gesperrt"
+
+#: waeup/kofa/students/interfaces.py:291
+msgid "CLR Activation Code"
+msgstr "CLR-Aktivierungs-Code"
+
+#: waeup/kofa/students/interfaces.py:304
+msgid "Nationality"
+msgstr "Nationalität"
+
+#: waeup/kofa/students/interfaces.py:313
+msgid "Employer"
+msgstr "Arbeitgeber"
+
+#: waeup/kofa/students/interfaces.py:323
+#, fuzzy
+msgid "Updated"
+msgstr "Alle Betten aktualisieren"
+
+#: waeup/kofa/students/interfaces.py:329 waeup/kofa/students/interfaces.py:347
+msgid "Permanent Address"
+msgstr "Postanschrift"
+
+#: waeup/kofa/students/interfaces.py:376
+msgid "Registr. or Matric. Number"
+msgstr "Registr.- oder Matrikelnummer"
+
+#: waeup/kofa/students/interfaces.py:396 waeup/kofa/students/interfaces.py:450
+#: waeup/kofa/browser/templates/showstudentspage.pt:7
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:59
+msgid "Certificate"
+msgstr "Zertifikat"
+
+#: waeup/kofa/students/interfaces.py:402 waeup/kofa/students/interfaces.py:482
+msgid "Entry Mode"
+msgstr "Studienmodus bei Einschreibung"
+
+#: waeup/kofa/students/interfaces.py:409 waeup/kofa/students/interfaces.py:463
+#: waeup/kofa/students/interfaces.py:489
+msgid "Entry Session"
+msgstr "Eingangssession"
+
+#: waeup/kofa/students/interfaces.py:431 waeup/kofa/students/interfaces.py:502
+msgid "Current Verdict"
+msgstr "Aktuelles Verdict"
+
+#: waeup/kofa/students/interfaces.py:438
+msgid "Previous Verdict"
+msgstr "Vorangegangenes Verdict"
+
+#: waeup/kofa/students/interfaces.py:520
+msgid "Bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:525 waeup/kofa/students/interfaces.py:552
+#, fuzzy
+msgid "Validated by"
+msgstr "entwertet"
+
+#: waeup/kofa/students/interfaces.py:545
+msgid "Verdict"
+msgstr "Verdict"
+
+#: waeup/kofa/students/interfaces.py:558
+#, fuzzy
+msgid "Validation Date"
+msgstr "Erstellungsdatum"
+
+#: waeup/kofa/students/interfaces.py:564
+#, fuzzy
+msgid "Total Credits"
+msgstr "Gesamte Credits"
+
+#: waeup/kofa/students/interfaces.py:602
+#, fuzzy
+msgid "Faculty Code"
+msgstr "Fakultät"
+
+#: waeup/kofa/students/interfaces.py:607
+#, fuzzy
+msgid "Department Code"
+msgstr "Department"
+
+#: waeup/kofa/students/interfaces.py:612
+#: waeup/kofa/university/interfaces.py:164
+msgid "Semester/Term"
+msgstr "Semester/Term"
+
+#: waeup/kofa/students/interfaces.py:618
+#: waeup/kofa/university/interfaces.py:158
+msgid "Passmark"
+msgstr "Passmark"
+
+#: waeup/kofa/students/interfaces.py:628
+#: waeup/kofa/browser/templates/certificatepage.pt:33
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:39
+msgid "Required"
+msgstr "Erforderlich"
+
+#: waeup/kofa/students/interfaces.py:640
+msgid "Carry-over Course"
+msgstr "Kurs aus vergangenem Studienjahr"
+
+#: waeup/kofa/students/interfaces.py:646
+msgid "Automatical Creation"
+msgstr "Automatische Erzeugung"
+
+#: waeup/kofa/students/interfaces.py:657
+#: waeup/kofa/university/interfaces.py:334
+#: waeup/kofa/university/interfaces.py:369
+#: waeup/kofa/students/browser_templates/transcript.pt:37
+msgid "Course"
+msgstr "Kurs"
+
+#: waeup/kofa/students/interfaces.py:673
+#, fuzzy
+msgid "Level Session"
+msgstr "Session"
+
+#: waeup/kofa/students/interfaces.py:702
+#, fuzzy
+msgid "Allocated Bed"
+msgstr "Student zuweisen"
+
+#: waeup/kofa/students/interfaces.py:708
+#, fuzzy
+msgid "Requested Bed Type"
+msgstr "Betttyp"
+
+#: waeup/kofa/students/interfaces.py:721
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr "Buchungsdatum"
+
+#: waeup/kofa/students/interfaces.py:727
+msgid "Booking Activation Code"
+msgstr "Aktivierungs-Code der Buchung"
+
+#: waeup/kofa/students/interfaces.py:748
+#, fuzzy
+msgid "Current Session Payment"
+msgstr "Aktuelle Session"
+
+#: waeup/kofa/students/interfaces.py:754 waeup/kofa/students/interfaces.py:795
+#: waeup/kofa/students/interfaces.py:819
+msgid "Payment Level"
+msgstr "Studienjahr bei Zahlung"
+
+#: waeup/kofa/students/interfaces.py:825
+msgid "Balance Amount"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:829
+msgid "Balance in Naira "
+msgstr ""
+
+#: waeup/kofa/students/reports.py:176
+#, fuzzy
+msgid "Create students report"
+msgstr "Studenten erzeugen"
+
+#: waeup/kofa/students/reports.py:196
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:122
+#, fuzzy
+msgid "Name:"
+msgstr "Name"
+
+#: waeup/kofa/students/utils.py:141
+#, fuzzy
+msgid "Study Course:"
+msgstr "Studiengang"
+
+#: waeup/kofa/students/utils.py:148 waeup/kofa/students/utils.py:734
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr "Department:"
+
+#: waeup/kofa/students/utils.py:157 waeup/kofa/students/utils.py:727
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr "Fakultät:"
+
+#: waeup/kofa/students/utils.py:166 waeup/kofa/students/utils.py:741
+#, fuzzy
+msgid "Entry Session:"
+msgstr "Eingangssession"
+
+#: waeup/kofa/students/utils.py:174
+#, fuzzy
+msgid "Date of Birth:"
+msgstr "Geburtsdatum"
+
+#: waeup/kofa/students/utils.py:261
+msgid "Scanned Documents"
+msgstr "Gescannte Dokumente"
+
+#: waeup/kofa/students/utils.py:338
+msgid "The previous session must not fall below your entry session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:343 waeup/kofa/students/utils.py:346
+msgid "This is not a previous session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:363 waeup/kofa/students/utils.py:402
+msgid "Study course data are incomplete."
+msgstr "Die Studiengangdaten sind unvollständig."
+
+#: waeup/kofa/students/utils.py:417
+#, fuzzy
+msgid "no bed allocated"
+msgstr "Unterkunftbeleg herunterladen"
+
+#: waeup/kofa/students/utils.py:419
+msgid "Amount could not be determined."
+msgstr "Betrag konnte nicht ermittelt werden."
+
+#: waeup/kofa/students/utils.py:452
+#, fuzzy
+msgid "Amount must be greater than 0."
+msgstr "Betrag konnte nicht ermittelt werden."
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:516
+msgid ""
+"This is to inform you that you have been provisionally admitted into ${a} as "
+"follows:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:556
+#, fuzzy
+msgid "Your Kofa student record was created on ${a}."
+msgstr "Student erzeugt."
+
+#: waeup/kofa/students/utils.py:608 waeup/kofa/students/viewlets.py:76
+#: waeup/kofa/students/viewlets.py:104
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:7
+msgid "Base Data"
+msgstr "Basisdaten"
+
+#: waeup/kofa/students/utils.py:628
+#, fuzzy
+msgid "Workflow History"
+msgstr "Workflow-Status"
+
+#: waeup/kofa/students/utils.py:721
+#, fuzzy
+msgid "Course of Study:"
+msgstr "Zugelassen für Studienfach:"
+
+#: waeup/kofa/students/utils.py:748
+#, fuzzy
+msgid "Entry Mode:"
+msgstr "Studienmodus bei Einschreibung"
+
+#: waeup/kofa/students/utils.py:755
+msgid "Cumulative GPA:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:786
+msgid "Error in image file."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:814
+msgid "(not yet)"
+msgstr "(noch nicht zugewiesen)"
+
+#: waeup/kofa/students/viewlets.py:114 waeup/kofa/students/viewlets.py:750
+msgid "Personal Data"
+msgstr "Persönliche Daten"
+
+#: waeup/kofa/students/viewlets.py:136 waeup/kofa/students/viewlets.py:755
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:27
+msgid "History"
+msgstr "Historie"
+
+#: waeup/kofa/students/viewlets.py:177
+#, fuzzy
+msgid "Trigger transition"
+msgstr "Keine Änderung"
+
+#: waeup/kofa/students/viewlets.py:186
+#, fuzzy
+msgid "Login as student"
+msgstr "nach Studenten"
+
+#: waeup/kofa/students/viewlets.py:195
+#, fuzzy
+msgid "Download admission letter"
+msgstr "Antrag herunterladen"
+
+#: waeup/kofa/students/viewlets.py:212
+msgid "Deactivate account"
+msgstr "Deaktiviere Konto"
+
+#: waeup/kofa/students/viewlets.py:224 waeup/kofa/students/viewlets.py:244
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:232
+msgid "Activate account"
+msgstr "Aktiviere Konto"
+
+#: waeup/kofa/students/viewlets.py:260 waeup/kofa/students/workflow.py:76
+msgid "Clear student"
+msgstr "Student einschreiben"
+
+#: waeup/kofa/students/viewlets.py:295
+msgid "Download clearance slip"
+msgstr "Einschreibungsbeleg herunterladen"
+
+#: waeup/kofa/students/viewlets.py:304 waeup/kofa/students/viewlets.py:313
+msgid "View"
+msgstr "Ansehen"
+
+#: waeup/kofa/students/viewlets.py:351
+#, fuzzy
+msgid "Transcript"
+msgstr "Studienfachwechsel"
+
+#: waeup/kofa/students/viewlets.py:382
+msgid "Reactivate this study course (revert previous transfer)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:411 waeup/kofa/students/workflow.py:160
+msgid "Validate courses"
+msgstr "Kurse validieren"
+
+#: waeup/kofa/students/viewlets.py:429
+msgid "Reject courses"
+msgstr "Kurseregistrierung zurückweisen"
+
+#: waeup/kofa/students/viewlets.py:448
+msgid "Download course registration slip"
+msgstr "Kurseregistrierungsbeleg herunterladen"
+
+#: waeup/kofa/students/viewlets.py:510
+msgid "Book accommodation"
+msgstr "Unterkunft buchen"
+
+#: waeup/kofa/students/viewlets.py:519
+msgid "Download bed allocation slip"
+msgstr "Unterkunftbeleg herunterladen"
+
+#: waeup/kofa/students/viewlets.py:528
+msgid "Relocate student"
+msgstr "Student umziehen"
+
+#: waeup/kofa/students/viewlets.py:554
+msgid "Change portrait"
+msgstr "Porträt ändern"
+
+#: waeup/kofa/students/viewlets.py:598
+msgid "Start new session"
+msgstr "Beginne neues Studienjahr"
+
+#: waeup/kofa/students/viewlets.py:612
+msgid "Add course list"
+msgstr "Kursliste hinzufügen"
+
+#: waeup/kofa/students/viewlets.py:631
+msgid "Edit course list"
+msgstr "Kursliste bearbeiten"
+
+#: waeup/kofa/students/viewlets.py:649
+#, fuzzy
+msgid "Add current session payment ticket"
+msgstr "Ticket für Online-Bezahlung hinzufügen"
+
+#: waeup/kofa/students/viewlets.py:657
+#, fuzzy
+msgid "Add previous session payment ticket"
+msgstr "Ticket für Online-Bezahlung hinzufügen"
+
+#: waeup/kofa/students/viewlets.py:672
+#, fuzzy
+msgid "Add balance payment ticket"
+msgstr "Ticket für Online-Bezahlung hinzufügen"
+
+#: waeup/kofa/students/viewlets.py:754
+msgid "Accommodation Data"
+msgstr "Unterkunft"
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:766
+msgid "${a} deleted."
+msgstr "${a} gelöscht."
+
+#: waeup/kofa/students/viewlets.py:780
+msgid "Uploaded file is too big."
+msgstr "Das hochgeladene Bild ist zu groß!"
+
+#: waeup/kofa/students/viewlets.py:786
+msgid "Could not determine file type."
+msgstr "Dateityp konnte nicht ermittelt werden."
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:791
+msgid "${a} file extension expected."
+msgstr "${a} Dateiendung erwartet."
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:797
+msgid "Only the following extensions are allowed: ${a}"
+msgstr "Nur die folgenden Erweiterungen sind erlaubt: ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:806
+msgid "File ${a} uploaded."
+msgstr "Datei ${a} hochgeladen."
+
+#: waeup/kofa/students/viewlets.py:824
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:10
+#: waeup/kofa/browser/templates/datacenterpage.pt:17
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:11
+msgid "File"
+msgstr "Datei"
+
+#: waeup/kofa/students/viewlets.py:825
+msgid "Scan"
+msgstr "Scan"
+
+#: waeup/kofa/students/viewlets.py:848
+msgid "Upload new file"
+msgstr "Neue Datei hochladen"
+
+#: waeup/kofa/students/viewlets.py:849
+msgid "Delete attachment"
+msgstr "Anhang löschen"
+
+#: waeup/kofa/students/viewlets.py:886
+msgid "No local file selected."
+msgstr "Keine lokale Datei ausgewählt."
+
+#: waeup/kofa/students/viewlets.py:90
+msgid "Application Slip"
+msgstr "Antrag"
+
+#: waeup/kofa/students/viewlets.py:900
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:9
+msgid "Passport Picture"
+msgstr "Passbild"
+
+#: waeup/kofa/students/viewlets.py:911
+msgid "Passport Picture (jpg only)"
+msgstr "Passbild (nur jpg)"
+
+#: waeup/kofa/students/viewlets.py:926 waeup/kofa/students/viewlets.py:937
+msgid "Birth Certificate"
+msgstr "Geburtsurkunde"
+
+#: waeup/kofa/students/viewlets.py:927 waeup/kofa/students/viewlets.py:938
+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: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:10
+#: waeup/kofa/browser/templates/departmentpage.pt:3
+#: waeup/kofa/browser/templates/certificatepage.pt:22
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:28
+msgid "Courses"
+msgstr "Kurse"
+
+#: waeup/kofa/university/export.py:165
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:13
+#: 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:146
+msgid "Title of course"
+msgstr "Titel des Kurses"
+
+#: waeup/kofa/university/interfaces.py:171
+#, fuzzy
+msgid "Former Course"
+msgstr "Cert.-Kurs"
+
+#: waeup/kofa/university/interfaces.py:172
+msgid ""
+"If this attribute is being set all certificate courses referring to this "
+"course will be automatically deleted."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:189
+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."
+
+#: waeup/kofa/university/interfaces.py:209
+msgid "Unnamed Certificate"
+msgstr "Unbenanntes Zertifikat"
+
+#: waeup/kofa/university/interfaces.py:214
+msgid "Study Mode"
+msgstr "Studienmodus"
+
+#: waeup/kofa/university/interfaces.py:221
+msgid "Start Level"
+msgstr "Einstiegslevel"
+
+#: waeup/kofa/university/interfaces.py:228
+msgid "End Level"
+msgstr "Abschlusslevel"
+
+#: waeup/kofa/university/interfaces.py:235
+msgid "Application Category"
+msgstr "Antragskategorie"
+
+#: waeup/kofa/university/interfaces.py:242
+msgid "Initial School Fee"
+msgstr "Schulgebühr bei Einschreibung"
+
+#: waeup/kofa/university/interfaces.py:247
+msgid "Returning School Fee"
+msgstr "Schulgebühr bei Rückmeldung"
+
+#: waeup/kofa/university/interfaces.py:252
+#, fuzzy
+msgid "Foreigner Initial School Fee"
+msgstr "Schulgebühr bei Einschreibung"
+
+#: waeup/kofa/university/interfaces.py:257
+#, fuzzy
+msgid "Foreigner Returning School Fee"
+msgstr "Schulgebühr bei Rückmeldung"
+
+#: waeup/kofa/university/interfaces.py:262
+msgid "Installment Ratio"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:269
+msgid "Custom Textline 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:274
+msgid "Custom Textline 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:279
+msgid "Custom Float 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:284
+msgid "Custom Float 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:296
+#: waeup/kofa/university/interfaces.py:298
+msgid "Start level and end level must correspond."
+msgstr "Einstiegs- und Abschlusslevel müssen übereinstimmen."
+
+#: waeup/kofa/university/interfaces.py:300
+#: waeup/kofa/university/interfaces.py:303
+msgid "Study mode, start level and end level must correspond."
+msgstr "Studienmodus, Einstiegs- und Abschlusslevel müssen korrespondieren."
+
+#: waeup/kofa/university/interfaces.py:313
+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."
+
+#: waeup/kofa/university/interfaces.py:347
+#, fuzzy
+msgid "Registration required"
+msgstr "Registrierungsnummer"
+
+#: waeup/kofa/university/interfaces.py:45
+msgid "Name of faculty"
+msgstr "Name der Fakultät"
+
+#: waeup/kofa/university/interfaces.py:46
+#: waeup/kofa/university/interfaces.py:94
+#: waeup/kofa/university/interfaces.py:147
+msgid "Unnamed"
+msgstr "Unbenannt"
+
+#: waeup/kofa/university/interfaces.py:51
+#: waeup/kofa/university/interfaces.py:99
+msgid "Name prefix"
+msgstr "Prefix"
+
+#: waeup/kofa/university/interfaces.py:67
+#: waeup/kofa/university/interfaces.py:118
+msgid "This code will become part of the URL."
+msgstr "Dieses Kürzel wird Teil der URL sein."
+
+#: waeup/kofa/university/interfaces.py:93
+msgid "Name of department"
+msgstr "Name des Departments"
+
+#: waeup/kofa/university/vocabularies.py:29
+msgid "Pre-Studies"
+msgstr "Vorstudium"
+
+#: waeup/kofa/university/vocabularies.py:30
+msgid "100 (Year 1)"
+msgstr "1. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:31
+msgid "200 (Year 2)"
+msgstr "2. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:32
+msgid "300 (Year 3)"
+msgstr "3. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:33
+msgid "400 (Year 4)"
+msgstr "4. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:34
+msgid "500 (Year 5)"
+msgstr "5. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:35
+msgid "600 (Year 6)"
+msgstr "6. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:36
+msgid "700 (Year 7)"
+msgstr "7. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:37
+msgid "800 (Year 8)"
+msgstr "8. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:38
+msgid "900 (Year 9)"
+msgstr "9. Studienjahr"
+
+#: waeup/kofa/university/vocabularies.py:39
+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:186
+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:239
+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:18
+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:21
+msgid "remove-selected-items"
+msgstr "Ausgewählte Elemente löschen"
+
+#~ 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/tags/0.2/src/waeup/kofa/locales/de/LC_MESSAGES/zope.po
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/de/LC_MESSAGES/zope.po	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/de/LC_MESSAGES/zope.po	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/locales/en/LC_MESSAGES/waeup.kofa.po
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/en/LC_MESSAGES/waeup.kofa.po	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/en/LC_MESSAGES/waeup.kofa.po	(revision 10886)
@@ -0,0 +1,6410 @@
+# #############################################################################
+#
+# 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: Thu Jun 27 09:36:46 2013\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:102
+#: waeup/kofa/accesscodes/browser.py:115
+#: waeup/kofa/applicants/browser.py:262
+#: waeup/kofa/applicants/browser.py:351
+#: waeup/kofa/applicants/browser.py:352
+#: waeup/kofa/applicants/browser.py:472
+#: waeup/kofa/browser/pages.py:636
+#: waeup/kofa/browser/pages.py:846
+#: waeup/kofa/browser/pages.py:872
+#: waeup/kofa/browser/pages.py:939
+#: waeup/kofa/browser/pages.py:1171
+#: waeup/kofa/browser/pages.py:1201
+#: waeup/kofa/browser/pages.py:1304
+#: waeup/kofa/browser/pages.py:1535
+#: waeup/kofa/browser/pages.py:1701
+#: waeup/kofa/browser/pages.py:1725
+#: waeup/kofa/browser/pages.py:1756
+#: waeup/kofa/browser/pages.py:1784
+#: waeup/kofa/browser/pages.py:1785
+#: waeup/kofa/browser/pages.py:1828
+#: waeup/kofa/browser/pages.py:1873
+#: waeup/kofa/browser/pages.py:1918
+#: waeup/kofa/browser/pages.py:1919
+#: waeup/kofa/browser/pages.py:1921
+#: waeup/kofa/browser/pages.py:1996
+#: waeup/kofa/browser/pages.py:2045
+#: waeup/kofa/browser/pages.py:2086
+#: waeup/kofa/browser/pages.py:2122
+#: waeup/kofa/browser/pages.py:2161
+#: waeup/kofa/browser/pages.py:2163
+#: waeup/kofa/browser/pages.py:2196
+#: waeup/kofa/browser/pages.py:2247
+#: waeup/kofa/browser/pages.py:2304
+#: waeup/kofa/students/browser.py:965
+#: waeup/kofa/students/browser.py:966
+#: waeup/kofa/students/browser.py:1341
+#: waeup/kofa/students/browser.py:1343
+#: waeup/kofa/students/browser.py:1500
+#: waeup/kofa/students/browser.py:1659
+#: waeup/kofa/students/browser.py:1697
+#: waeup/kofa/students/browser.py:1730
+msgid "Cancel"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:104
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:114
+msgid "Reimport"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:117
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:122
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:128
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:138
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:141
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:154
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:155
+#: waeup/kofa/applicants/browser.py:85
+#: waeup/kofa/applicants/browser.py:113
+#: waeup/kofa/browser/pages.py:725
+#: waeup/kofa/browser/pages.py:1495
+#: waeup/kofa/browser/viewlets.py:200
+#: waeup/kofa/students/browser.py:258
+#: waeup/kofa/students/browser.py:299
+msgid "Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:156
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:157
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:182
+#: waeup/kofa/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:184
+msgid "${a} disabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:188
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:192
+msgid "re-enabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:194
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:198
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:237
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:246
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:254
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:263
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:44
+msgid "Archive"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:45
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:47
+#: waeup/kofa/accesscodes/browser.py:208
+#: waeup/kofa/accesscodes/browser.py:228
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:54
+msgid "No batch selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:62
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:70
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:79
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:85
+msgid "Create batch"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:93
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:95
+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:23
+msgid "Serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:24
+msgid "AC"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:26
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:48
+msgid "Owner"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:5
+msgid "for access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:7
+msgid "with PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:8
+msgid "with serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:9
+msgid "with message term"
+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:119
+msgid "Applicant has not yet been admitted."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:125
+msgid "Registration Number exists."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:128
+msgid "No course admitted provided."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/applicant.py:160
+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:1002
+msgid "1st and 2nd choice must be different."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1020
+msgid "The form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1026
+msgid "Form has been submitted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:1069
+msgid "Apply for ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1074
+msgid "Outside application period."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1089
+#: waeup/kofa/browser/pages.py:2353
+#: waeup/kofa/students/browser.py:2700
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1112
+#: waeup/kofa/students/browser.py:2717
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1118
+#: waeup/kofa/applicants/browser.py:1130
+msgid "No application record found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:112
+#: waeup/kofa/applicants/viewlets.py:90
+msgid "Search applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1122
+#: waeup/kofa/students/browser.py:2726
+msgid "Your password has already been set and used. Please proceed to the login page."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1141
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1158
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:128
+#: waeup/kofa/students/browser.py:276
+#: waeup/kofa/students/browser.py:319
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:133
+msgid "No applicant found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:141
+#: waeup/kofa/applicants/viewlets.py:99
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:144
+#: waeup/kofa/applicants/browser.py:219
+#: waeup/kofa/applicants/browser.py:351
+#: waeup/kofa/applicants/browser.py:404
+#: waeup/kofa/applicants/browser.py:795
+#: waeup/kofa/applicants/browser.py:841
+#: waeup/kofa/applicants/browser.py:949
+#: waeup/kofa/applicants/browser.py:952
+#: waeup/kofa/applicants/browser.py:996
+#: waeup/kofa/browser/pages.py:607
+#: waeup/kofa/browser/pages.py:764
+#: waeup/kofa/browser/pages.py:787
+#: waeup/kofa/browser/pages.py:866
+#: waeup/kofa/browser/pages.py:1533
+#: waeup/kofa/browser/pages.py:1784
+#: waeup/kofa/browser/pages.py:1824
+#: waeup/kofa/browser/pages.py:1918
+#: waeup/kofa/browser/pages.py:1967
+#: waeup/kofa/browser/pages.py:2118
+#: waeup/kofa/browser/pages.py:2161
+#: waeup/kofa/browser/pages.py:2182
+#: waeup/kofa/hostels/browser.py:131
+#: waeup/kofa/hostels/browser.py:176
+#: waeup/kofa/hostels/browser.py:241
+#: waeup/kofa/hostels/browser.py:265
+#: waeup/kofa/hostels/browser.py:360
+#: waeup/kofa/students/browser.py:542
+#: waeup/kofa/students/browser.py:595
+#: waeup/kofa/students/browser.py:770
+#: waeup/kofa/students/browser.py:872
+#: waeup/kofa/students/browser.py:965
+#: waeup/kofa/students/browser.py:993
+#: waeup/kofa/students/browser.py:1341
+#: waeup/kofa/students/browser.py:1386
+#: waeup/kofa/students/browser.py:2173
+#: waeup/kofa/students/browser.py:2188
+#: waeup/kofa/students/browser.py:2311
+msgid "Save"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:145
+#: waeup/kofa/applicants/browser.py:175
+#: waeup/kofa/applicants/browser.py:352
+#: waeup/kofa/applicants/browser.py:423
+#: waeup/kofa/browser/pages.py:767
+#: waeup/kofa/browser/pages.py:809
+#: waeup/kofa/browser/pages.py:893
+#: waeup/kofa/browser/pages.py:1701
+#: waeup/kofa/browser/pages.py:1712
+#: waeup/kofa/browser/pages.py:1785
+#: waeup/kofa/browser/pages.py:1816
+#: waeup/kofa/hostels/browser.py:134
+#: waeup/kofa/hostels/browser.py:149
+#: waeup/kofa/students/browser.py:300
+#: waeup/kofa/students/browser.py:1840
+#: waeup/kofa/students/browser.py:1853
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:145
+#: waeup/kofa/applicants/browser.py:200
+#: waeup/kofa/applicants/browser.py:231
+#: waeup/kofa/applicants/browser.py:241
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:146
+#: waeup/kofa/applicants/browser.py:209
+#: waeup/kofa/applicants/browser.py:354
+#: waeup/kofa/applicants/browser.py:481
+#: waeup/kofa/browser/pages.py:1786
+#: waeup/kofa/browser/pages.py:1842
+#: waeup/kofa/browser/pages.py:1922
+#: waeup/kofa/browser/pages.py:2005
+#: waeup/kofa/browser/pages.py:2164
+#: waeup/kofa/browser/pages.py:2215
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:147
+#: waeup/kofa/applicants/browser.py:205
+#: waeup/kofa/applicants/browser.py:355
+#: waeup/kofa/applicants/browser.py:477
+#: waeup/kofa/browser/pages.py:1787
+#: waeup/kofa/browser/pages.py:1838
+#: waeup/kofa/browser/pages.py:1923
+#: waeup/kofa/browser/pages.py:2001
+#: waeup/kofa/browser/pages.py:2165
+#: waeup/kofa/browser/pages.py:2211
+msgid "Add local role"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:148
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:181
+msgid "No container selected!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:192
+#: waeup/kofa/applicants/browser.py:440
+#: waeup/kofa/applicants/browser.py:896
+msgid "Could not delete:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:195
+#: waeup/kofa/applicants/browser.py:443
+#: waeup/kofa/applicants/browser.py:899
+#: waeup/kofa/browser/pages.py:153
+#: waeup/kofa/students/browser.py:346
+#: waeup/kofa/students/browser.py:1055
+#: waeup/kofa/students/browser.py:1407
+#: waeup/kofa/students/browser.py:1602
+#: waeup/kofa/students/browser.py:1873
+#: waeup/kofa/students/browser.py:2549
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:223
+#: waeup/kofa/applicants/browser.py:418
+#: waeup/kofa/applicants/browser.py:869
+#: waeup/kofa/applicants/browser.py:1005
+#: waeup/kofa/browser/pages.py:195
+#: waeup/kofa/browser/pages.py:2295
+#: waeup/kofa/hostels/browser.py:55
+#: waeup/kofa/students/browser.py:100
+#: waeup/kofa/students/browser.py:564
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:250
+msgid "An applicants container for the same application type and entrance year exists already in the database."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:258
+msgid "Added:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:270
+#: 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:353
+#: waeup/kofa/applicants/browser.py:448
+msgid "Create students from selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:361
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:429
+#: waeup/kofa/applicants/browser.py:454
+msgid "No applicant selected!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:465
+#: waeup/kofa/applicants/browser.py:614
+msgid "${a} students successfully created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:468
+#: waeup/kofa/applicants/browser.py:617
+msgid "No student could be created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:495
+#: waeup/kofa/applicants/viewlets.py:115
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:498
+msgid "Create application record"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:503
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:517
+msgid "Applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:541
+#: waeup/kofa/students/browser.py:438
+msgid "set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:542
+#: waeup/kofa/students/browser.py:439
+msgid "unset"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:547
+msgid "${a} <br /> Application Record ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:637
+#: waeup/kofa/students/utils.py:426
+#: waeup/kofa/students/utils.py:459
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:649
+#: waeup/kofa/students/browser.py:1655
+#: waeup/kofa/students/browser.py:1693
+#: waeup/kofa/students/browser.py:1726
+msgid "Payment ticket created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:672
+#: waeup/kofa/students/browser.py:1749
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:68
+msgid "'You can not edit your application records after final submission. You really want to submit?'"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:717
+#: waeup/kofa/students/browser.py:1806
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:723
+#: waeup/kofa/students/browser.py:1812
+msgid "Online Payment Slip"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:749
+msgid "Please pay and submit before trying to download the application slip."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:770
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:775
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:795
+#: waeup/kofa/applicants/browser.py:952
+#: waeup/kofa/applicants/browser.py:1008
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:796
+#: waeup/kofa/applicants/browser.py:908
+#: waeup/kofa/applicants/browser.py:950
+#: waeup/kofa/applicants/browser.py:985
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:796
+#: waeup/kofa/applicants/browser.py:913
+#: waeup/kofa/applicants/browser.py:950
+#: waeup/kofa/applicants/browser.py:953
+#: waeup/kofa/applicants/browser.py:990
+#: waeup/kofa/students/browser.py:1343
+#: waeup/kofa/students/browser.py:1391
+#: waeup/kofa/students/browser.py:1583
+#: waeup/kofa/students/browser.py:2557
+msgid "Remove selected tickets"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:826
+msgid "${a} <br /> Application Form ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:838
+#: waeup/kofa/students/browser.py:592
+msgid "No transition"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:84
+msgid "Application Section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:883
+#: waeup/kofa/students/browser.py:1589
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:961
+#: waeup/kofa/students/browser.py:108
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:977
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:979
+msgid "Passport picture confirmation box not ticked."
+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:28
+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:25
+msgid "Passport Photo:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:50
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:66
+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:82
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:40
+msgid "Application Fee Payment Tickets"
+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/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:46
+msgid "Application Number"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:50
+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:92
+msgid "note_acmp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:93
+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:38
+msgid "note_armp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:56
+msgid "Records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:6
+msgid "Introduction"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:12
+msgid "Application Period"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:50
+msgid "Year"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:11
+msgid "with email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:5
+msgid "for applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/export.py:39
+msgid "Basic Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:143
+msgid "Create Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:144
+msgid "Update Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:174
+#: waeup/kofa/applicants/interfaces.py:238
+msgid "Human readable description in HTML format"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:192
+#: waeup/kofa/students/browser.py:1130
+#: waeup/kofa/students/browser.py:1290
+#: waeup/kofa/university/interfaces.py:38
+#: waeup/kofa/university/interfaces.py:66
+#: waeup/kofa/university/interfaces.py:86
+#: waeup/kofa/university/interfaces.py:117
+#: waeup/kofa/university/interfaces.py:139
+#: waeup/kofa/university/interfaces.py:188
+#: waeup/kofa/university/interfaces.py:201
+#: waeup/kofa/university/interfaces.py:311
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:39
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:68
+#: waeup/kofa/browser/templates/facultypage.pt:8
+#: waeup/kofa/browser/templates/departmentpage.pt:15
+#: waeup/kofa/browser/templates/departmentpage.pt:38
+#: waeup/kofa/browser/templates/searchpage.pt:13
+#: waeup/kofa/browser/templates/facultymanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:15
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:26
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:52
+msgid "Code"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:198
+#: waeup/kofa/students/browser.py:1131
+#: waeup/kofa/students/browser.py:1291
+#: waeup/kofa/students/interfaces.py:597
+#: waeup/kofa/university/interfaces.py:208
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:70
+#: waeup/kofa/browser/templates/facultypage.pt:9
+#: waeup/kofa/browser/templates/departmentpage.pt:16
+#: waeup/kofa/browser/templates/departmentpage.pt:39
+#: waeup/kofa/browser/templates/searchpage.pt:14
+#: waeup/kofa/browser/templates/certificatepage.pt:32
+#: waeup/kofa/browser/templates/facultymanagepage.pt:43
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:38
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:37
+#: waeup/kofa/students/browser_templates/transcript.pt:38
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:16
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:27
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:54
+msgid "Title"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:204
+#: waeup/kofa/applicants/interfaces.py:318
+msgid "Application Target"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:211
+#: waeup/kofa/applicants/interfaces.py:325
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:218
+msgid "Application Mode"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:232
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:251
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:253
+#: waeup/kofa/applicants/interfaces.py:259
+#: waeup/kofa/hostels/interfaces.py:39
+#: waeup/kofa/hostels/interfaces.py:45
+msgid "Example:"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:257
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:263
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:269
+#: waeup/kofa/interfaces.py:652
+msgid "Application Fee"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:275
+msgid "Hide container"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:353
+#: waeup/kofa/students/interfaces.py:185
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:359
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:26
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:364
+#: waeup/kofa/students/interfaces.py:225
+#: waeup/kofa/students/interfaces.py:356
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:375
+#: waeup/kofa/applicants/interfaces.py:526
+#: waeup/kofa/students/interfaces.py:204
+#: waeup/kofa/students/interfaces.py:381
+msgid "First Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:379
+#: waeup/kofa/students/interfaces.py:209
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:383
+#: waeup/kofa/students/interfaces.py:214
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:387
+#: waeup/kofa/students/interfaces.py:297
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:393
+#: waeup/kofa/students/interfaces.py:219
+msgid "Sex"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:398
+#: waeup/kofa/applicants/interfaces.py:474
+#: waeup/kofa/applicants/interfaces.py:531
+#: waeup/kofa/browser/interfaces.py:221
+#: waeup/kofa/interfaces.py:435
+#: waeup/kofa/interfaces.py:511
+#: waeup/kofa/students/interfaces.py:386
+msgid "Email Address"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:403
+#: waeup/kofa/interfaces.py:440
+#: waeup/kofa/interfaces.py:518
+#: waeup/kofa/students/interfaces.py:250
+msgid "Phone"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:408
+#: waeup/kofa/applicants/interfaces.py:479
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:413
+#: waeup/kofa/applicants/interfaces.py:484
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:425
+#: waeup/kofa/applicants/interfaces.py:495
+msgid "Notice"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:429
+#: waeup/kofa/students/interfaces.py:199
+#: waeup/kofa/browser/templates/showstudentspage.pt:4
+#: waeup/kofa/students/browser_templates/containerpage.pt:32
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:38
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:56
+msgid "Student Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:434
+#: waeup/kofa/applicants/interfaces.py:489
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:439
+msgid "Form locked"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:53
+#: waeup/kofa/applicants/workflow.py:74
+#: waeup/kofa/students/payments.py:115
+msgid "Payment approved"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:66
+#: waeup/kofa/students/payments.py:104
+msgid "Successful payment"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:73
+#: waeup/kofa/students/payments.py:122
+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:40
+msgid "Desired Study Courses"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:41
+msgid "Process Data"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:56
+#: waeup/kofa/students/browser.py:139
+#: waeup/kofa/students/utils.py:356
+#: waeup/kofa/students/utils.py:383
+#: waeup/kofa/students/utils.py:394
+#: waeup/kofa/students/utils.py:450
+msgid "Session configuration object is not available."
+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:146
+#: waeup/kofa/applicants/viewlets.py:155
+msgid "Create students"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:163
+msgid "View application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:171
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:179
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:197
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:213
+#: waeup/kofa/applicants/workflow.py:108
+#: waeup/kofa/students/browser.py:361
+msgid "Create student record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:230
+#: waeup/kofa/students/viewlets.py:480
+msgid "Download payment slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:245
+#: waeup/kofa/applicants/workflow.py:73
+#: waeup/kofa/students/viewlets.py:495
+#: 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:728
+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:91
+msgid "admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:44
+msgid "not admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:45
+#: waeup/kofa/interfaces.py:90
+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:93
+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:652
+msgid "Academics"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:127
+#: waeup/kofa/browser/pages.py:460
+msgid "Administration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:134
+#: waeup/kofa/browser/viewlets.py:335
+#: waeup/kofa/browser/viewlets.py:703
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:152
+#: waeup/kofa/browser/pages.py:507
+#: waeup/kofa/browser/viewlets.py:344
+#: waeup/kofa/browser/viewlets.py:713
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:159
+#: waeup/kofa/browser/pages.py:885
+#: waeup/kofa/browser/pages.py:1493
+#: waeup/kofa/browser/viewlets.py:353
+#: waeup/kofa/browser/viewlets.py:722
+msgid "Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:166
+#: waeup/kofa/browser/reports.py:37
+#: waeup/kofa/browser/viewlets.py:362
+#: waeup/kofa/browser/viewlets.py:731
+msgid "Reports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:174
+#: waeup/kofa/students/browser.py:2910
+msgid "Student Data Exports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:94
+msgid "Home"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:212
+msgid "Unique Identifier"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:213
+msgid "User Name, Student or Applicant Id, Matriculation or Registration Number"
+msgstr ""
+
+#: waeup/kofa/browser/layout.py:97
+msgid "'Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1009
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1011
+#: waeup/kofa/browser/pages.py:2385
+#: waeup/kofa/students/browser.py:491
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1019
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1033
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1041
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1052
+#: waeup/kofa/browser/pages.py:1059
+msgid "Your file contains forbidden characters. Please replace."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1169
+#: waeup/kofa/browser/pages.py:1199
+#: waeup/kofa/browser/pages.py:1302
+#: waeup/kofa/browser/pages.py:1415
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1183
+#: waeup/kofa/browser/pages.py:1267
+#: waeup/kofa/browser/pages.py:1391
+msgid "Import aborted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:119
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1202
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1203
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1246
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1254
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1279
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1305
+#: waeup/kofa/browser/pages.py:1534
+msgid "Reset"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1306
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1307
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1308
+msgid "Perform import"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:132
+#: waeup/kofa/browser/pages.py:899
+#: waeup/kofa/hostels/browser.py:290
+#: waeup/kofa/hostels/browser.py:320
+#: waeup/kofa/students/browser.py:333
+msgid "No item selected."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1373
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1417
+msgid "Process next"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1467
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1469
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1477
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1479
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1480
+msgid "Show"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1496
+msgid "Back"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1497
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1505
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1520
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1523
+msgid "No search results found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1531
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1551
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1554
+msgid "Given storage path cannot be used."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1555
+msgid "Error: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1558
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1570
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1572
+msgid "Create CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1597
+#: waeup/kofa/students/browser.py:2923
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1689
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1701
+#: waeup/kofa/browser/pages.py:1720
+#: waeup/kofa/browser/pages.py:1737
+#: waeup/kofa/browser/pages.py:1741
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1702
+#: waeup/kofa/university/export.py:36
+msgid "Faculties"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1706
+#: waeup/kofa/browser/viewlets.py:419
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1715
+msgid "You are not allowed to remove entire faculties."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1748
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1773
+#: waeup/kofa/browser/pages.py:1783
+#: waeup/kofa/university/export.py:78
+msgid "Departments"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1785
+#: waeup/kofa/browser/pages.py:1833
+#: waeup/kofa/browser/pages.py:1852
+#: waeup/kofa/browser/pages.py:1856
+msgid "Add department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1793
+#: waeup/kofa/browser/viewlets.py:446
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1819
+msgid "You are not allowed to remove entire departments."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1863
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1866
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1888
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1919
+#: waeup/kofa/browser/pages.py:1971
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1919
+#: waeup/kofa/browser/pages.py:1986
+#: waeup/kofa/browser/pages.py:2015
+#: waeup/kofa/browser/pages.py:2019
+msgid "Add course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1920
+#: waeup/kofa/browser/pages.py:1977
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1920
+#: waeup/kofa/browser/pages.py:1991
+#: waeup/kofa/browser/pages.py:2056
+#: waeup/kofa/browser/pages.py:2060
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1929
+#: waeup/kofa/browser/viewlets.py:453
+msgid "Manage department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1980
+msgid "You are not allowed to remove certificates."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2031
+msgid "A course with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2037
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2072
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2078
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2113
+#: waeup/kofa/browser/viewlets.py:483
+msgid "Edit course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2155
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2162
+#: waeup/kofa/browser/pages.py:2191
+#: waeup/kofa/browser/pages.py:2228
+#: waeup/kofa/browser/pages.py:2230
+msgid "Add certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2163
+#: waeup/kofa/browser/pages.py:2186
+msgid "Remove selected certificate courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2169
+#: waeup/kofa/browser/viewlets.py:513
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2235
+msgid "The chosen certificate course is already part of this certificate."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2239
+msgid "certificate course ${a}_${b} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2280
+#: waeup/kofa/browser/viewlets.py:543
+msgid "Edit certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2283
+msgid "Save and return"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2316
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2364
+msgid "No record found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2378
+#: waeup/kofa/students/browser.py:2748
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2382
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:246
+#: waeup/kofa/browser/viewlets.py:281
+#: waeup/kofa/browser/templates/staffsitelayout.pt:28
+#: waeup/kofa/browser/templates/studentsitelayout.pt:32
+msgid "Login"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:257
+msgid "You logged in."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:266
+msgid "Your personal data record is outdated. Please update."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:300
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:307
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:309
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:324
+msgid "You have been logged out. Thanks for using WAeUP Kofa!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:365
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:371
+#: waeup/kofa/browser/pages.py:652
+msgid "Send message now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:390
+#: waeup/kofa/browser/pages.py:423
+#: waeup/kofa/browser/pages.py:669
+#: waeup/kofa/students/browser.py:489
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:410
+msgid "Send now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:425
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:450
+msgid "<h1>Welcome to WAeUP.Kofa</h1>"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:508
+#: waeup/kofa/hostels/browser.py:228
+#: waeup/kofa/students/viewlets.py:168
+#: waeup/kofa/students/viewlets.py:252
+#: waeup/kofa/students/viewlets.py:321
+#: waeup/kofa/students/viewlets.py:337
+#: waeup/kofa/students/viewlets.py:396
+#: waeup/kofa/students/viewlets.py:463
+msgid "Manage"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:509
+#: waeup/kofa/browser/viewlets.py:190
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:517
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:554
+#: waeup/kofa/browser/pages.py:556
+#: waeup/kofa/browser/viewlets.py:549
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:578
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:599
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:633
+msgid "User settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:650
+#: waeup/kofa/students/browser.py:470
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:679
+#: waeup/kofa/browser/viewlets.py:369
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:692
+#: waeup/kofa/browser/viewlets.py:385
+msgid "My Roles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:723
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:750
+msgid "View portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:763
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:764
+#: waeup/kofa/browser/pages.py:814
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:766
+#: waeup/kofa/browser/pages.py:793
+#: waeup/kofa/browser/pages.py:826
+msgid "Add session configuration"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:805
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:817
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:830
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:841
+msgid "The session chosen already exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:863
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:910
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:913
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:924
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:935
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:938
+msgid "Upload"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:984
+msgid "Import Manager"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:986
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:99
+msgid "No local role selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:990
+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/pdf.py:153
+msgid "Date:"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:204
+#: waeup/kofa/browser/pdf.py:461
+#: waeup/kofa/browser/pdf.py:471
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:12
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:16
+msgid "Date"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:522
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:791
+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
+#: waeup/kofa/students/browser_templates/exportconfig_course.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:6
+msgid "Certificate Courses"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:29
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:35
+#: waeup/kofa/students/browser_templates/transcript.pt:36
+msgid "Semester"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:36
+msgid "Cert. Course"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:37
+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:8
+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:34
+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/datacenterexportpage.pt:44
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:4
+msgid "Export Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:45
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:5
+msgid "Exporter"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:46
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:6
+msgid "Filter"
+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: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:16
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:18
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:41
+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:24
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:13
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:37
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:15
+msgid "Header"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:38
+msgid "change to"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:39
+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:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:14
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:36
+msgid "Reload page"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:12
+msgid "Overwrite contents in new location?"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:3
+#: waeup/kofa/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:7
+msgid "Copy existing data to new storage?"
+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:18
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:14
+msgid "Datasets"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:19
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:11
+msgid "Upload Date"
+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:113
+msgid "Download"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:22
+msgid "Processor:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:35
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:59
+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:67
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:72
+msgid "Processor"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:73
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:74
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:75
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:76
+msgid "CSV Skeleton File"
+msgstr ""
+
+#: waeup/kofa/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:101
+#: waeup/kofa/browser/templates/facultymanagepage.pt:75
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:97
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:112
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:91
+msgid "Local Role"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:16
+#: waeup/kofa/browser/templates/userscontainerpage.pt:7
+#: waeup/kofa/browser/templates/facultymanagepage.pt:16
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:17
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:16
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:6
+#: waeup/kofa/browser/templates/facultymanagepage.pt:6
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:5
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:7
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:9
+msgid "Settings"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:99
+#: waeup/kofa/browser/templates/userscontainerpage.pt:5
+#: waeup/kofa/browser/templates/facultymanagepage.pt:73
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:110
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:89
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:22
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:39
+msgid "Name"
+msgstr ""
+
+#: waeup/kofa/browser/templates/facultypage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/kofa/browser/templates/fileupload.pt:26
+#: waeup/kofa/students/browser_templates/imageupload.pt:20
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:32
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:13
+#: waeup/kofa/browser/templates/usereditformpage.pt:9
+#: waeup/kofa/students/browser_templates/basepage.pt:14
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:18
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:27
+#: waeup/kofa/students/browser_templates/change_password.pt:10
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:31
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:27
+msgid "Password:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:24
+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:36
+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:39
+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:44
+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:48
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:5
+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/reportscontainercreate.pt:22
+msgid "Type of report to create"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:41
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:76
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:36
+msgid "<img src=\"${DYNAMIC_CONTENT}\" /> Reload"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:51
+msgid "Create new report"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:6
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:47
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:7
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:15
+msgid "Creator"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:7
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:48
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:91
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:47
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:16
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:17
+msgid "Department"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:17
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:46
+msgid "Type"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:9
+#: waeup/kofa/students/browser_templates/containerpage.pt:24
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:18
+msgid "Search Results"
+msgstr ""
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:10
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:62
+msgid "Comment"
+msgstr ""
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:5
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:57
+msgid "Fullname"
+msgstr ""
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:6
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:8
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:49
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:58
+msgid "Status"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:113
+#: waeup/kofa/browser/templates/studentsitelayout.pt:97
+msgid "Back to bottom"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:122
+#: waeup/kofa/browser/templates/studentsitelayout.pt:107
+msgid "Back to top"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:124
+#: waeup/kofa/browser/templates/studentsitelayout.pt:109
+msgid "Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:127
+#: waeup/kofa/browser/templates/studentsitelayout.pt:112
+msgid "Copyright &copy; WAeUP Group 2008-2013"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:34
+#: waeup/kofa/browser/templates/studentsitelayout.pt:38
+msgid "Contact"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:4
+#: waeup/kofa/browser/templates/studentsitelayout.pt:4
+msgid "WAeUP.Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:40
+#: waeup/kofa/browser/templates/studentsitelayout.pt:44
+msgid "Logout"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:62
+msgid "Student Id:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:66
+msgid "State:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/usereditformpage.pt:15
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:24
+#: waeup/kofa/students/browser_templates/change_password.pt:16
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:44
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:180
+msgid "Add"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:320
+msgid "Any link text"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:402
+#: waeup/kofa/students/viewlets.py:160
+msgid "Send email"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:411
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:428
+#: waeup/kofa/browser/viewlets.py:474
+#: waeup/kofa/browser/viewlets.py:504
+#: waeup/kofa/browser/viewlets.py:534
+msgid "Export student data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:437
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:463
+#: waeup/kofa/browser/viewlets.py:493
+#: waeup/kofa/browser/viewlets.py:523
+msgid "Show students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:558
+msgid "Show logs"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:567
+msgid "Upload data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:576
+msgid "Process data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:585
+msgid "Export data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:594
+msgid "View processed files"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:667
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:95
+#: waeup/kofa/browser/viewlets.py:170
+#: waeup/kofa/students/viewlets.py:329
+#: waeup/kofa/students/viewlets.py:536
+#: waeup/kofa/students/viewlets.py:583
+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:163
+#: waeup/kofa/hostels/browser.py:190
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:133
+#: waeup/kofa/hostels/browser.py:168
+msgid "Clear all hostels"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:171
+msgid "All hostels cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:179
+msgid "Settings have been saved."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:193
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:202
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:204
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:239
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:242
+#: waeup/kofa/hostels/browser.py:270
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:243
+#: waeup/kofa/hostels/browser.py:284
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:244
+#: waeup/kofa/hostels/browser.py:314
+msgid "Release selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:245
+#: waeup/kofa/hostels/browser.py:341
+msgid "Clear hostel"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:275
+msgid "${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:308
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:332
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:337
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:344
+msgid "Hostel cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:357
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:363
+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/containerpage.pt:21
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:37
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:45
+msgid "Id"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:23
+msgid "Booked Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:47
+msgid "Number"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:6
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:66
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelpage.pt:5
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:19
+msgid "Reserved Beds:"
+msgstr ""
+
+#: waeup/kofa/hostels/export.py:61
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:9
+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:105
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:111
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:117
+msgid "Reserved Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:128
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:135
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:142
+msgid "Beds for Pre-Study Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:149
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:156
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:163
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:170
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:177
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:188
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:198
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:220
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:226
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:232
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:237
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:238
+msgid "Enter valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:253
+msgid "Either student does not exist or student is not in accommodation session."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/interfaces.py:260
+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:93
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:99
+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 K"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:63
+msgid "Block L"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:64
+msgid "Block M"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:65
+msgid "Block N"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:66
+msgid "Block O"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:67
+msgid "Block P"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:68
+msgid "Block Q"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:69
+msgid "Block R"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:70
+msgid "Block S"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:71
+msgid "Block T"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:72
+msgid "Block U"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:73
+msgid "Block V"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:74
+msgid "Block W"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:207
+msgid "Subject"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:211
+#: waeup/kofa/students/browser.py:1134
+#: waeup/kofa/students/browser.py:1297
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:10
+msgid "Grade"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:368
+msgid "Processor name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:394
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:401
+msgid "Email to:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:408
+msgid "Subject:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:412
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:416
+msgid "Text:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:445
+#: waeup/kofa/interfaces.py:501
+msgid "Public Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:455
+#: waeup/kofa/interfaces.py:456
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:462
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:463
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:492
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:97
+#: waeup/kofa/browser/templates/userscontainerpage.pt:4
+#: waeup/kofa/browser/templates/facultymanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:93
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:108
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:87
+msgid "User Id"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:497
+#: waeup/kofa/students/browser_templates/containerpage.pt:36
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:42
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:48
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:29
+msgid "Full Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:507
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:524
+#: waeup/kofa/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:571
+msgid "Name of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:572
+msgid "Sample University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:577
+msgid "Abbreviated Title of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:583
+msgid "Skin"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:59
+msgid "new"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:590
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:60
+msgid "completed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:602
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:608
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:61
+msgid "pending"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:615
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:616
+msgid "Kofa Contact"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:62
+msgid "active"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:621
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:628
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:63
+msgid "assigned"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:635
+msgid "Carry-over Course Registration"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:64
+msgid "callbacks"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:644
+#: waeup/kofa/interfaces.py:690
+msgid "Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:658
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:664
+msgid "Bed Booking Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:670
+msgid "Rent"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:676
+msgid "Clearance enabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:92
+msgid "clearance started"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:93
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:94
+msgid "cleared"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:95
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:96
+msgid "courses registered"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:97
+msgid "courses validated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:98
+msgid "returning"
+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. Proceed to the login page and enter your credentials."
+msgstr ""
+
+#: waeup/kofa/objecthistory.py:74
+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:782
+#: waeup/kofa/students/interfaces.py:806
+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:789
+#: waeup/kofa/students/interfaces.py:813
+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/applicants/browser_templates/applicanteditpage.pt:92
+#: 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:853
+msgid "System"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1024
+msgid "You must select a session for the level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1033
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1035
+#: waeup/kofa/students/browser.py:2477
+msgid "This level exists."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1045
+msgid "No study level selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1073
+msgid "You are not allowed to view the transcript."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1088
+msgid "${a}: Transcript Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1106
+msgid "You are not allowed to download the transcript."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1121
+#: waeup/kofa/students/viewlets.py:366
+msgid "Academic Transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1125
+msgid "CERTIFIED TRUE COPY"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1129
+#: waeup/kofa/students/browser.py:1289
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:35
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:25
+msgid "Sem."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1132
+#: waeup/kofa/students/interfaces.py:623
+#: waeup/kofa/university/interfaces.py:152
+#: waeup/kofa/students/browser_templates/transcript.pt:39
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:19
+msgid "Credits"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1133
+#: waeup/kofa/students/browser.py:1296
+#: waeup/kofa/students/interfaces.py:634
+#: waeup/kofa/students/interfaces.py:667
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:20
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:32
+msgid "Score"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1161
+#: waeup/kofa/students/viewlets.py:203
+msgid "Transfer student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1171
+msgid "Transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1175
+msgid "Current level does not match certificate levels."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1177
+msgid "Former study course record incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1179
+msgid "Maximum number of transfers exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1181
+msgid "Successfully transferred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1191
+msgid "Revert previous transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1196
+msgid "No previous transfer."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1201
+msgid "Revert now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1204
+msgid "Previous transfer reverted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1235
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1256
+msgid "Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1262
+msgid "1st Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1268
+msgid "2nd Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1274
+msgid "Level Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1283
+msgid "Course Registration Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1292
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:38
+msgid "Dept."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1293
+#: waeup/kofa/browser/templates/searchpage.pt:15
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:22
+msgid "Faculty"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1294
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:40
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:30
+msgid "Cred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1342
+#: waeup/kofa/students/browser.py:1481
+#: waeup/kofa/students/browser.py:1492
+#: waeup/kofa/students/browser.py:2602
+msgid "Add course ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1360
+#: waeup/kofa/students/browser.py:2507
+msgid "No valid course code entered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1366
+#: waeup/kofa/students/browser.py:2512
+msgid "Course not found."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1383
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1397
+#: waeup/kofa/students/browser.py:2536
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:141
+msgid "Clearance is disabled for this session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1427
+#: waeup/kofa/students/browser.py:1452
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1431
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1433
+#: waeup/kofa/students/browser.py:1464
+msgid "Student is in the wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1457
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1461
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1516
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:152
+msgid "Total credits exceed ${a}."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1541
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1574
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:159
+msgid "The ticket exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:161
+msgid "Successfully added ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1622
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1630
+#: waeup/kofa/students/browser.py:1680
+#: waeup/kofa/students/browser.py:1711
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1640
+#: waeup/kofa/students/browser.py:1931
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1646
+msgid "You have not yet booked accommodation."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1670
+msgid "Add previous session online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1675
+msgid "No previous payment to be made."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1708
+msgid "Add balance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:182
+#: waeup/kofa/students/export.py:197
+#: waeup/kofa/students/reports.py:154
+#: waeup/kofa/students/viewlets.py:688
+msgid "Students"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1844
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1856
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1863
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1895
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1897
+msgid "Create bed ticket"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1914
+msgid "Outside booking period: ${a} - ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1917
+msgid "Outside booking period."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1921
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1925
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1936
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1949
+#: waeup/kofa/students/browser.py:2268
+#: waeup/kofa/students/browser.py:2410
+msgid "Activation code is invalid."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1961
+#: waeup/kofa/students/browser.py:1976
+msgid "System error: Please contact the adminsitrator."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1981
+#: waeup/kofa/students/browser.py:2117
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1987
+#: waeup/kofa/students/browser.py:2271
+#: waeup/kofa/students/browser.py:2414
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1990
+#: waeup/kofa/students/browser.py:2275
+#: waeup/kofa/students/browser.py:2417
+#: waeup/kofa/students/browser.py:2662
+msgid "invalidated"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1995
+#: waeup/kofa/students/browser.py:2279
+#: waeup/kofa/students/browser.py:2422
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2007
+#: waeup/kofa/students/browser.py:2131
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2016
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2033
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2052
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:206
+#: waeup/kofa/students/viewlets.py:119
+#: waeup/kofa/students/viewlets.py:751
+msgid "Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2061
+msgid "Bed Allocation Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:208
+msgid "Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2089
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2095
+msgid "Student can't be relocated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2139
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:214
+#: waeup/kofa/students/viewlets.py:125
+#: waeup/kofa/students/viewlets.py:752
+msgid "Payments"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2158
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2170
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2185
+#: waeup/kofa/students/viewlets.py:545
+msgid "Change password"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2199
+msgid "Password changed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2211
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2226
+#: waeup/kofa/students/viewlets.py:569
+#: waeup/kofa/students/workflow.py:48
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2230
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2248
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2252
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2256
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2283
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:229
+#: waeup/kofa/students/viewlets.py:131
+msgid "Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2293
+msgid "Edit clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2314
+#: waeup/kofa/students/browser.py:2328
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2322
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2339
+#: waeup/kofa/students/browser.py:2373
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2342
+#: waeup/kofa/students/browser.py:2376
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2351
+#: waeup/kofa/students/workflow.py:62
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2352
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2355
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2367
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:238
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2385
+msgid "Start session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2389
+msgid "Start now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2398
+msgid "You are not entitled to start session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2435
+msgid "An error occurred, please contact the system administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2437
+msgid "Session started."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2456
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2468
+msgid "Create course list now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2479
+msgid "Your data are incomplete"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2524
+msgid "Edit course list of ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2565
+msgid "You are a postgraduate student, your course list can't bee registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:257
+msgid "Student Section"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2573
+msgid "Maximum credits of ${a} exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2578
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2582
+msgid "Register course list"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2618
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2621
+msgid "Set"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2636
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2644
+msgid "Access code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2647
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2653
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2659
+msgid "Access code has already been used."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2668
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2680
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2723
+#: waeup/kofa/students/browser.py:2734
+msgid "No student record found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2768
+msgid "Your password request was successful."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2784
+#: waeup/kofa/students/interfaces.py:417
+#: waeup/kofa/students/interfaces.py:508
+#: waeup/kofa/browser/templates/showstudentspage.pt:8
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:60
+msgid "Current Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2785
+#: waeup/kofa/students/interfaces.py:424
+#: waeup/kofa/students/interfaces.py:456
+#: waeup/kofa/students/interfaces.py:514
+#: waeup/kofa/browser/templates/showstudentspage.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:61
+msgid "Current Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:282
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2836
+msgid "in any session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2842
+msgid "at any level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:287
+#: waeup/kofa/students/browser.py:331
+#: waeup/kofa/students/browser.py:2633
+#: waeup/kofa/students/browser.py:2845
+msgid "No student found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2878
+#: waeup/kofa/students/interfaces.py:539
+#: waeup/kofa/students/interfaces.py:714
+#: waeup/kofa/students/utils.py:224
+#: waeup/kofa/students/browser_templates/transcript.pt:35
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:8
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:14
+msgid "Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2879
+#: waeup/kofa/students/utils.py:226
+#: waeup/kofa/university/interfaces.py:340
+#: waeup/kofa/browser/templates/certificatepage.pt:28
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:34
+#: waeup/kofa/students/browser_templates/transcript.pt:34
+msgid "Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2936
+msgid "Configure student data export"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2944
+msgid "All Sessions"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2951
+msgid "All Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2958
+msgid "All Modes"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:298
+#: waeup/kofa/students/viewlets.py:144
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3022
+msgid "Export started for students with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3113
+msgid "Export started for course tickets with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:358
+#: waeup/kofa/students/viewlets.py:151
+msgid "Add student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:366
+msgid "Student record created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:380
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:402
+msgid "Login now"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:406
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:412
+msgid "You successfully logged in as student."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:430
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:432
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:509
+msgid "Admission Letter of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:527
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:576
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:616
+msgid "Student account has been activated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:635
+msgid "Student account has been deactivated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:670
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:700
+#: waeup/kofa/students/viewlets.py:109
+#: waeup/kofa/students/viewlets.py:749
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:6
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:706
+msgid "Clearance Slip of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:715
+msgid "Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:716
+msgid "Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:723
+msgid "Date, Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:724
+msgid "Date, Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:745
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:789
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:791
+#: waeup/kofa/students/browser.py:826
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:803
+#: waeup/kofa/students/viewlets.py:277
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:815
+msgid "Save comment and reject clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:819
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:823
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:856
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:869
+msgid "Manage personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:884
+msgid "Edit personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:887
+msgid "Save/Confirm"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:914
+msgid "${a}: Study Course"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:917
+msgid "${a}: Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:945
+msgid "First Study Course, "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:947
+msgid "Second Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:952
+msgid "First Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:963
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:966
+#: waeup/kofa/students/browser.py:1039
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:967
+#: waeup/kofa/students/browser.py:1019
+msgid "Add study level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:999
+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/clearanceeditpage.pt:8
+msgid "Scans"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:31
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:38
+msgid "Select all"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:10
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:10
+msgid "with registration number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:11
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:11
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:12
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:12
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:78
+msgid "in session"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:13
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:13
+msgid "in department"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:14
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:14
+msgid "in study mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:15
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:15
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:33
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:27
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:34
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:40
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:35
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:41
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:15
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:25
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "State"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:5
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:5
+msgid "for students"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:8
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:8
+msgid "with id"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:9
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:9
+msgid "with name"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:18
+msgid "Weight"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:20
+msgid "The activation code can be found on the payment slip."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:34
+msgid "Your data have been approved. You can continue by clicking the button below."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:48
+msgid "Current Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig.pt:1
+msgid "<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."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:12
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:2
+#: waeup/kofa/students/browser_templates/exportconfig.pt:12
+msgid "Exporter (Data Type):"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:24
+#: waeup/kofa/students/browser_templates/exportconfig.pt:24
+msgid "Current Session:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:36
+#: waeup/kofa/students/browser_templates/exportconfig.pt:36
+msgid "Current Level:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:14
+msgid "Level Session:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:50
+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:93
+#: 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:94
+#: 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:90
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:46
+msgid "Payment Id"
+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: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:32
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:33
+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/studentsreportgeneratorpage.pt:10
+msgid "Study Modes Group:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.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/browser_templates/studentsreportgeneratorpage.pt:21
+msgid "Set study modes filter."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.pt:33
+msgid "<option value=\"${DYNAMIC_CONTENT}\"> ${DYNAMIC_CONTENT} </option>"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.pt:37
+msgid "Select academic session to report."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:6
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:9
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:50
+msgid "Previous Study Courses:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:64
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:30
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:69
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:35
+msgid "Level Code"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:70
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:36
+msgid "Level Title"
+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: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:39
+msgid "Fact."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:31
+msgid "Requ."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:43
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:21
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:34
+msgid "CO"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:44
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:35
+msgid "Auto"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:6
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:87
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:62
+msgid "Enter valid course code."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:93
+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:28
+msgid "Dep."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:29
+msgid "Fac."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:58
+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/export.py:220
+msgid "Student Study Courses"
+msgstr ""
+
+#: waeup/kofa/students/export.py:250
+msgid "Student Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/export.py:275
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:9
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:30
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:20
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:309
+msgid "Student Payments"
+msgstr ""
+
+#: waeup/kofa/students/export.py:346
+msgid "Payment Data for Bursary"
+msgstr ""
+
+#: waeup/kofa/students/export.py:377
+msgid "Bed Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:413
+msgid "Student Payments Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:442
+msgid "Student Study Levels Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:471
+msgid "Combo Card Data"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:191
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:193
+msgid "This message will be shown if and only if deactivated students try to login."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:232
+#: waeup/kofa/students/interfaces.py:365
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:239
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:245
+msgid "Email"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:280
+msgid "Officer's Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:285
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:291
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:304
+msgid "Nationality"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:313
+msgid "Employer"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:323
+msgid "Updated"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:329
+#: waeup/kofa/students/interfaces.py:347
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:376
+msgid "Registr. or Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:396
+#: waeup/kofa/students/interfaces.py:450
+#: waeup/kofa/browser/templates/showstudentspage.pt:7
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:59
+msgid "Certificate"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:402
+#: waeup/kofa/students/interfaces.py:482
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:409
+#: waeup/kofa/students/interfaces.py:463
+#: waeup/kofa/students/interfaces.py:489
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:431
+#: waeup/kofa/students/interfaces.py:502
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:438
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:520
+msgid "Bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:525
+#: waeup/kofa/students/interfaces.py:552
+msgid "Validated by"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:545
+msgid "Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:558
+msgid "Validation Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:564
+msgid "Total Credits"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:602
+msgid "Faculty Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:607
+msgid "Department Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:612
+#: waeup/kofa/university/interfaces.py:164
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:618
+#: waeup/kofa/university/interfaces.py:158
+msgid "Passmark"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:628
+#: waeup/kofa/browser/templates/certificatepage.pt:33
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:39
+msgid "Required"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:640
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:646
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:657
+#: waeup/kofa/university/interfaces.py:334
+#: waeup/kofa/university/interfaces.py:369
+#: waeup/kofa/students/browser_templates/transcript.pt:37
+msgid "Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:673
+msgid "Level Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:702
+msgid "Allocated Bed"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:708
+msgid "Requested Bed Type"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:721
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:727
+msgid "Booking Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:748
+msgid "Current Session Payment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:754
+#: waeup/kofa/students/interfaces.py:795
+#: waeup/kofa/students/interfaces.py:819
+msgid "Payment Level"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:825
+msgid "Balance Amount"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:829
+msgid "Balance in Naira "
+msgstr ""
+
+#: waeup/kofa/students/reports.py:176
+msgid "Create students report"
+msgstr ""
+
+#: waeup/kofa/students/reports.py:196
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:122
+msgid "Name:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:141
+msgid "Study Course:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:148
+#: waeup/kofa/students/utils.py:734
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:157
+#: waeup/kofa/students/utils.py:727
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:166
+#: waeup/kofa/students/utils.py:741
+msgid "Entry Session:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:174
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:261
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:338
+msgid "The previous session must not fall below your entry session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:343
+#: waeup/kofa/students/utils.py:346
+msgid "This is not a previous session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:363
+#: waeup/kofa/students/utils.py:402
+msgid "Study course data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:417
+msgid "no bed allocated"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:419
+msgid "Amount could not be determined."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:452
+msgid "Amount must be greater than 0."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:516
+msgid "This is to inform you that you have been provisionally admitted into ${a} as follows:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:556
+msgid "Your Kofa student record was created on ${a}."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:608
+#: waeup/kofa/students/viewlets.py:76
+#: waeup/kofa/students/viewlets.py:104
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:7
+msgid "Base Data"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:628
+msgid "Workflow History"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:721
+msgid "Course of Study:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:748
+msgid "Entry Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:755
+msgid "Cumulative GPA:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:786
+msgid "Error in image file."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:814
+msgid "(not yet)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:114
+#: waeup/kofa/students/viewlets.py:750
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:136
+#: waeup/kofa/students/viewlets.py:755
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:27
+msgid "History"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:177
+msgid "Trigger transition"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:186
+msgid "Login as student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:195
+msgid "Download admission letter"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:212
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:224
+#: waeup/kofa/students/viewlets.py:244
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:232
+msgid "Activate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:260
+#: waeup/kofa/students/workflow.py:76
+msgid "Clear student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:295
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:304
+#: waeup/kofa/students/viewlets.py:313
+msgid "View"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:351
+msgid "Transcript"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:382
+msgid "Reactivate this study course (revert previous transfer)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:411
+#: waeup/kofa/students/workflow.py:160
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:429
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:448
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:510
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:519
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:528
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:554
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:598
+msgid "Start new session"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:612
+msgid "Add course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:631
+msgid "Edit course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:649
+msgid "Add current session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:657
+msgid "Add previous session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:672
+msgid "Add balance payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:754
+msgid "Accommodation Data"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:766
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:780
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:786
+msgid "Could not determine file type."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:791
+msgid "${a} file extension expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:797
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:806
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:824
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:10
+#: waeup/kofa/browser/templates/datacenterpage.pt:17
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:11
+msgid "File"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:825
+msgid "Scan"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:848
+msgid "Upload new file"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:849
+msgid "Delete attachment"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:886
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:90
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:900
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:9
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:911
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:926
+#: waeup/kofa/students/viewlets.py:937
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:927
+#: waeup/kofa/students/viewlets.py:938
+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: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:10
+#: waeup/kofa/browser/templates/departmentpage.pt:3
+#: waeup/kofa/browser/templates/certificatepage.pt:22
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:28
+msgid "Courses"
+msgstr ""
+
+#: waeup/kofa/university/export.py:165
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:13
+#: 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:146
+msgid "Title of course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:171
+msgid "Former Course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:172
+msgid "If this attribute is being set all certificate courses referring to this course will be automatically deleted."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:189
+msgid "Enter unique course code which will become part of the URL."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:209
+msgid "Unnamed Certificate"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:214
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:221
+msgid "Start Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:228
+msgid "End Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:235
+msgid "Application Category"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:242
+msgid "Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:247
+msgid "Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:252
+msgid "Foreigner Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:257
+msgid "Foreigner Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:262
+msgid "Installment Ratio"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:269
+msgid "Custom Textline 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:274
+msgid "Custom Textline 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:279
+msgid "Custom Float 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:284
+msgid "Custom Float 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:296
+#: waeup/kofa/university/interfaces.py:298
+msgid "Start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:300
+#: waeup/kofa/university/interfaces.py:303
+msgid "Study mode, start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:313
+msgid "Enter unique certificate code which will become part of the URL."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:347
+msgid "Registration required"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:45
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:46
+#: waeup/kofa/university/interfaces.py:94
+#: waeup/kofa/university/interfaces.py:147
+msgid "Unnamed"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:51
+#: waeup/kofa/university/interfaces.py:99
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:67
+#: waeup/kofa/university/interfaces.py:118
+msgid "This code will become part of the URL."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:93
+msgid "Name of department"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:29
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:30
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:31
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:32
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:33
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:34
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:35
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:36
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:37
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:38
+msgid "900 (Year 9)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:39
+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:186
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:239
+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:18
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/kofa/widgets/sequencewidget.pt:21
+msgid "remove-selected-items"
+msgstr ""
+
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/en/LC_MESSAGES/zope.po
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/en/LC_MESSAGES/zope.po	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/en/LC_MESSAGES/zope.po	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/locales/fr/LC_MESSAGES/waeup.kofa.po
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/fr/LC_MESSAGES/waeup.kofa.po	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/fr/LC_MESSAGES/waeup.kofa.po	(revision 10886)
@@ -0,0 +1,6860 @@
+# #############################################################################
+#
+# 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: Thu Jun 27 09:36:46 2013\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:102 waeup/kofa/accesscodes/browser.py:115
+#: waeup/kofa/applicants/browser.py:262 waeup/kofa/applicants/browser.py:351
+#: waeup/kofa/applicants/browser.py:352 waeup/kofa/applicants/browser.py:472
+#: waeup/kofa/browser/pages.py:636 waeup/kofa/browser/pages.py:846
+#: waeup/kofa/browser/pages.py:872 waeup/kofa/browser/pages.py:939
+#: waeup/kofa/browser/pages.py:1171 waeup/kofa/browser/pages.py:1201
+#: waeup/kofa/browser/pages.py:1304 waeup/kofa/browser/pages.py:1535
+#: waeup/kofa/browser/pages.py:1701 waeup/kofa/browser/pages.py:1725
+#: waeup/kofa/browser/pages.py:1756 waeup/kofa/browser/pages.py:1784
+#: waeup/kofa/browser/pages.py:1785 waeup/kofa/browser/pages.py:1828
+#: waeup/kofa/browser/pages.py:1873 waeup/kofa/browser/pages.py:1918
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1921
+#: waeup/kofa/browser/pages.py:1996 waeup/kofa/browser/pages.py:2045
+#: waeup/kofa/browser/pages.py:2086 waeup/kofa/browser/pages.py:2122
+#: waeup/kofa/browser/pages.py:2161 waeup/kofa/browser/pages.py:2163
+#: waeup/kofa/browser/pages.py:2196 waeup/kofa/browser/pages.py:2247
+#: waeup/kofa/browser/pages.py:2304 waeup/kofa/students/browser.py:965
+#: waeup/kofa/students/browser.py:966 waeup/kofa/students/browser.py:1341
+#: waeup/kofa/students/browser.py:1343 waeup/kofa/students/browser.py:1500
+#: waeup/kofa/students/browser.py:1659 waeup/kofa/students/browser.py:1697
+#: waeup/kofa/students/browser.py:1730
+msgid "Cancel"
+msgstr "Annuller"
+
+#: waeup/kofa/accesscodes/browser.py:104
+msgid "Batch creation cancelled."
+msgstr "Création du lot annullée"
+
+#: waeup/kofa/accesscodes/browser.py:114
+msgid "Reimport"
+msgstr "Réimporter"
+
+#: waeup/kofa/accesscodes/browser.py:117
+msgid "Reimport Access Code Batches"
+msgstr "Réimportez les lots de code d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:122
+msgid "Reimport cancelled."
+msgstr "Réimportez annuller"
+
+#: waeup/kofa/accesscodes/browser.py:128
+msgid "No file chosen. Action cancelled."
+msgstr "Aucun fichier selectionné. Action est annulée."
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:138
+msgid "This batch already exists: ${a}"
+msgstr "Ce lot existe déjà: ${a}"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:141
+msgid "Successfully reimported: ${a}"
+msgstr "Réimportation terminée avec succès: ${a}"
+
+#: waeup/kofa/accesscodes/browser.py:154
+msgid "Search and Manage Access Codes"
+msgstr "Recherche et Gestion des codes d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:155 waeup/kofa/applicants/browser.py:85
+#: waeup/kofa/applicants/browser.py:113 waeup/kofa/browser/pages.py:725
+#: waeup/kofa/browser/pages.py:1495 waeup/kofa/browser/viewlets.py:200
+#: waeup/kofa/students/browser.py:258 waeup/kofa/students/browser.py:299
+msgid "Search"
+msgstr "Recherche "
+
+#: waeup/kofa/accesscodes/browser.py:156
+msgid "Disable ACs"
+msgstr "Déconnectez ACs"
+
+#: waeup/kofa/accesscodes/browser.py:157
+msgid "Enable ACs"
+msgstr "Activez ACs"
+
+#: waeup/kofa/accesscodes/browser.py:182 waeup/kofa/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr "Désactivé(e)"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:184
+msgid "${a} disabled."
+msgstr "${a} désactivé(e)"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:188
+msgid "${a}: Disable transition not allowed."
+msgstr "${a}: Désactivation de Transition non autorisée."
+
+#: waeup/kofa/accesscodes/browser.py:192
+msgid "re-enabled"
+msgstr "Disponible à nouveau"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:194
+msgid "${a} (re-)enabled."
+msgstr "${a} redisponible à nouveau."
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:198
+msgid "${a}: Re-enable transition not allowed."
+msgstr "${a}: Transition initiale non permise"
+
+#: waeup/kofa/accesscodes/browser.py:237
+msgid "Add Access Code Batch"
+msgstr "Ajoutez le lot du code d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:246
+msgid "Reimport Access Code Batch"
+msgstr "Réimporter le lot du code d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:254
+msgid "Search Access Codes"
+msgstr "Recherche des codes d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:263
+msgid "Access Codes"
+msgstr "Codes d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:44
+msgid "Archive"
+msgstr "Archive"
+
+#: waeup/kofa/accesscodes/browser.py:45
+msgid "Archive and delete"
+msgstr "Archivage et suppression"
+
+#: waeup/kofa/accesscodes/browser.py:47 waeup/kofa/accesscodes/browser.py:208
+#: waeup/kofa/accesscodes/browser.py:228
+msgid "Access Code Batches"
+msgstr "Lots de code d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:54
+msgid "No batch selected."
+msgstr "Aucun lot sélectionné."
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:62
+msgid "Archived ${a} (${b})"
+msgstr "Archivée ${a} (${b})"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:70
+msgid "Deleted batch ${a}"
+msgstr "Lot supprimé ${a}"
+
+#: waeup/kofa/accesscodes/browser.py:79
+msgid "Create Access Code Batch"
+msgstr "Créer le lot de code d'accès"
+
+#: waeup/kofa/accesscodes/browser.py:85
+msgid "Create batch"
+msgstr "Créer le lot"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:93
+msgid "Batch created (${a} entries)"
+msgstr "Lot créé (entrées ${a})"
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:95
+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:23
+msgid "Serial"
+msgstr "Série"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:24
+msgid "AC"
+msgstr "AC"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:26
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:48
+msgid "Owner"
+msgstr "Propriétaire"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:5
+msgid "for access codes"
+msgstr "pour les codes d'accès"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:7
+msgid "with PIN"
+msgstr "avec PIN"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:8
+msgid "with serial"
+msgstr "avec série"
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:9
+msgid "with message term"
+msgstr "avec fin de message"
+
+#: 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:119
+#, fuzzy
+msgid "Applicant has not yet been admitted."
+msgstr "La demande n'a pas encore commencé."
+
+#: waeup/kofa/applicants/applicant.py:125
+#, fuzzy
+msgid "Registration Number exists."
+msgstr "Numéro d’Inscription"
+
+#: waeup/kofa/applicants/applicant.py:128
+#, fuzzy
+msgid "No course admitted provided."
+msgstr "Aucun portrait chargé."
+
+#. Default: ""
+#: waeup/kofa/applicants/applicant.py:160
+#, 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:1002
+msgid "1st and 2nd choice must be different."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1020
+#, fuzzy
+msgid "The form cannot be submitted. Wrong state!"
+msgstr "Ce formulaire ne peut pas être validé. État incorrect!"
+
+#: waeup/kofa/applicants/browser.py:1026
+msgid "Form has been submitted."
+msgstr "Le formulaire a été validé."
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:1069
+msgid "Apply for ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1074
+#, fuzzy
+msgid "Outside application period."
+msgstr "Aperçu des données de la demande"
+
+#: waeup/kofa/applicants/browser.py:1089 waeup/kofa/browser/pages.py:2353
+#: waeup/kofa/students/browser.py:2700
+#, fuzzy
+msgid "Send login credentials to email address"
+msgstr "Obtenir les autorisations d'ouverture"
+
+#: waeup/kofa/applicants/browser.py:1112 waeup/kofa/students/browser.py:2717
+#, fuzzy
+msgid "An error occurred."
+msgstr "Une erreur du serveur smtp produite."
+
+#: waeup/kofa/applicants/browser.py:1118 waeup/kofa/applicants/browser.py:1130
+#, fuzzy
+msgid "No application record found."
+msgstr "Aperçu des données de la demande"
+
+#: waeup/kofa/applicants/browser.py:112 waeup/kofa/applicants/viewlets.py:90
+#, fuzzy
+msgid "Search applicants"
+msgstr "Démarrer la demande"
+
+#: waeup/kofa/applicants/browser.py:1122 waeup/kofa/students/browser.py:2726
+#, 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:1141
+msgid "You have successfully been registered for the"
+msgstr "Vous avez été inscrit avec succès au"
+
+#: waeup/kofa/applicants/browser.py:1158
+msgid "Your registration was successful."
+msgstr "Votre inscription a été enregistrée avec succès."
+
+#: waeup/kofa/applicants/browser.py:128 waeup/kofa/students/browser.py:276
+#: waeup/kofa/students/browser.py:319
+msgid "Empty search string"
+msgstr "Chaîne de recherche vide"
+
+#: waeup/kofa/applicants/browser.py:133
+#, fuzzy
+msgid "No applicant found."
+msgstr "Aperçu des données de la demande"
+
+#: waeup/kofa/applicants/browser.py:141 waeup/kofa/applicants/viewlets.py:99
+msgid "Manage application section"
+msgstr "Gerer la zone de demande"
+
+#: waeup/kofa/applicants/browser.py:144 waeup/kofa/applicants/browser.py:219
+#: waeup/kofa/applicants/browser.py:351 waeup/kofa/applicants/browser.py:404
+#: waeup/kofa/applicants/browser.py:795 waeup/kofa/applicants/browser.py:841
+#: waeup/kofa/applicants/browser.py:949 waeup/kofa/applicants/browser.py:952
+#: waeup/kofa/applicants/browser.py:996 waeup/kofa/browser/pages.py:607
+#: waeup/kofa/browser/pages.py:764 waeup/kofa/browser/pages.py:787
+#: waeup/kofa/browser/pages.py:866 waeup/kofa/browser/pages.py:1533
+#: waeup/kofa/browser/pages.py:1784 waeup/kofa/browser/pages.py:1824
+#: waeup/kofa/browser/pages.py:1918 waeup/kofa/browser/pages.py:1967
+#: waeup/kofa/browser/pages.py:2118 waeup/kofa/browser/pages.py:2161
+#: waeup/kofa/browser/pages.py:2182 waeup/kofa/hostels/browser.py:131
+#: waeup/kofa/hostels/browser.py:176 waeup/kofa/hostels/browser.py:241
+#: waeup/kofa/hostels/browser.py:265 waeup/kofa/hostels/browser.py:360
+#: waeup/kofa/students/browser.py:542 waeup/kofa/students/browser.py:595
+#: waeup/kofa/students/browser.py:770 waeup/kofa/students/browser.py:872
+#: waeup/kofa/students/browser.py:965 waeup/kofa/students/browser.py:993
+#: waeup/kofa/students/browser.py:1341 waeup/kofa/students/browser.py:1386
+#: waeup/kofa/students/browser.py:2173 waeup/kofa/students/browser.py:2188
+#: waeup/kofa/students/browser.py:2311
+msgid "Save"
+msgstr "Sauvegarder"
+
+#: waeup/kofa/applicants/browser.py:145 waeup/kofa/applicants/browser.py:175
+#: waeup/kofa/applicants/browser.py:352 waeup/kofa/applicants/browser.py:423
+#: waeup/kofa/browser/pages.py:767 waeup/kofa/browser/pages.py:809
+#: waeup/kofa/browser/pages.py:893 waeup/kofa/browser/pages.py:1701
+#: waeup/kofa/browser/pages.py:1712 waeup/kofa/browser/pages.py:1785
+#: waeup/kofa/browser/pages.py:1816 waeup/kofa/hostels/browser.py:134
+#: waeup/kofa/hostels/browser.py:149 waeup/kofa/students/browser.py:300
+#: waeup/kofa/students/browser.py:1840 waeup/kofa/students/browser.py:1853
+msgid "Remove selected"
+msgstr "Supprimer sélectionné"
+
+#: waeup/kofa/applicants/browser.py:145 waeup/kofa/applicants/browser.py:200
+#: waeup/kofa/applicants/browser.py:231 waeup/kofa/applicants/browser.py:241
+msgid "Add applicants container"
+msgstr "Ajoutez le récipient du candidat"
+
+#: waeup/kofa/applicants/browser.py:146 waeup/kofa/applicants/browser.py:209
+#: waeup/kofa/applicants/browser.py:354 waeup/kofa/applicants/browser.py:481
+#: waeup/kofa/browser/pages.py:1786 waeup/kofa/browser/pages.py:1842
+#: waeup/kofa/browser/pages.py:1922 waeup/kofa/browser/pages.py:2005
+#: waeup/kofa/browser/pages.py:2164 waeup/kofa/browser/pages.py:2215
+msgid "Remove selected local roles"
+msgstr "Supprimer les rôles locaux sélectionnés"
+
+#: waeup/kofa/applicants/browser.py:147 waeup/kofa/applicants/browser.py:205
+#: waeup/kofa/applicants/browser.py:355 waeup/kofa/applicants/browser.py:477
+#: waeup/kofa/browser/pages.py:1787 waeup/kofa/browser/pages.py:1838
+#: waeup/kofa/browser/pages.py:1923 waeup/kofa/browser/pages.py:2001
+#: waeup/kofa/browser/pages.py:2165 waeup/kofa/browser/pages.py:2211
+msgid "Add local role"
+msgstr "Ajouter rôle local"
+
+#: waeup/kofa/applicants/browser.py:148
+msgid "Applicants Containers"
+msgstr "Espace Candidat"
+
+#: waeup/kofa/applicants/browser.py:181
+#, fuzzy
+msgid "No container selected!"
+msgstr "Aucun ticket sélectionné."
+
+#: waeup/kofa/applicants/browser.py:192 waeup/kofa/applicants/browser.py:440
+#: waeup/kofa/applicants/browser.py:896
+msgid "Could not delete:"
+msgstr "Ne pourrait être supprimer:"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:195 waeup/kofa/applicants/browser.py:443
+#: waeup/kofa/applicants/browser.py:899 waeup/kofa/browser/pages.py:153
+#: waeup/kofa/students/browser.py:346 waeup/kofa/students/browser.py:1055
+#: waeup/kofa/students/browser.py:1407 waeup/kofa/students/browser.py:1602
+#: waeup/kofa/students/browser.py:1873 waeup/kofa/students/browser.py:2549
+msgid "Successfully removed: ${a}"
+msgstr "Supprimer avec succès: ${a}"
+
+#: waeup/kofa/applicants/browser.py:223 waeup/kofa/applicants/browser.py:418
+#: waeup/kofa/applicants/browser.py:869 waeup/kofa/applicants/browser.py:1005
+#: waeup/kofa/browser/pages.py:195 waeup/kofa/browser/pages.py:2295
+#: waeup/kofa/hostels/browser.py:55 waeup/kofa/students/browser.py:100
+#: waeup/kofa/students/browser.py:564
+msgid "Form has been saved."
+msgstr "Formulaire a été Sauvegardé"
+
+#: waeup/kofa/applicants/browser.py:250
+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:258
+msgid "Added:"
+msgstr "Ajouté:"
+
+#: waeup/kofa/applicants/browser.py:270 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:353 waeup/kofa/applicants/browser.py:448
+#, fuzzy
+msgid "Create students from selected"
+msgstr "Créer les données d’étudiant"
+
+#: waeup/kofa/applicants/browser.py:361
+msgid "Manage applicants container"
+msgstr "Gérer les blocs des demandes"
+
+#: waeup/kofa/applicants/browser.py:429 waeup/kofa/applicants/browser.py:454
+msgid "No applicant selected!"
+msgstr "Aucun demandeur sélectionné !"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:465 waeup/kofa/applicants/browser.py:614
+#, fuzzy
+msgid "${a} students successfully created."
+msgstr "Cours ${a} créé avec succès."
+
+#: waeup/kofa/applicants/browser.py:468 waeup/kofa/applicants/browser.py:617
+#, fuzzy
+msgid "No student could be created."
+msgstr "Données étudiant créé."
+
+#: waeup/kofa/applicants/browser.py:495 waeup/kofa/applicants/viewlets.py:115
+msgid "Add applicant"
+msgstr "Ajoutez Candidat"
+
+#: waeup/kofa/applicants/browser.py:498
+msgid "Create application record"
+msgstr "Créer les données de la demande"
+
+#: waeup/kofa/applicants/browser.py:503
+msgid "Applicant record created."
+msgstr "Données de la demande créer."
+
+#: waeup/kofa/applicants/browser.py:517
+msgid "Applicant"
+msgstr "Demandeur "
+
+#: waeup/kofa/applicants/browser.py:541 waeup/kofa/students/browser.py:438
+msgid "set"
+msgstr "Prêt"
+
+#: waeup/kofa/applicants/browser.py:542 waeup/kofa/students/browser.py:439
+msgid "unset"
+msgstr "Non prêt"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:547
+#, fuzzy
+msgid "${a} <br /> Application Record ${b}"
+msgstr "${a} Données de la demande ${b}"
+
+#: waeup/kofa/applicants/browser.py:637 waeup/kofa/students/utils.py:426
+#: waeup/kofa/students/utils.py:459
+msgid "This type of payment has already been made."
+msgstr "Ce type de paiement a été déjà fait."
+
+#: waeup/kofa/applicants/browser.py:649 waeup/kofa/students/browser.py:1655
+#: waeup/kofa/students/browser.py:1693 waeup/kofa/students/browser.py:1726
+msgid "Payment ticket created."
+msgstr "Ticket de paiement créé."
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:672 waeup/kofa/students/browser.py:1749
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr "${a}: Ticket de paiement en ligne ${b}"
+
+#: waeup/kofa/applicants/browser.py:68
+msgid ""
+"'You can not edit your application records after final submission. You "
+"really want to submit?'"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:717 waeup/kofa/students/browser.py:1806
+msgid "Payment Data"
+msgstr "Données de paiement"
+
+#: waeup/kofa/applicants/browser.py:723 waeup/kofa/students/browser.py:1812
+#, fuzzy
+msgid "Online Payment Slip"
+msgstr "Reçu du paiement en ligne"
+
+#: waeup/kofa/applicants/browser.py:749
+msgid "Please pay and submit before trying to download the application slip."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:770
+msgid "Uploaded image is too big!"
+msgstr "Image trop grande pour téléchargement!"
+
+#: waeup/kofa/applicants/browser.py:775
+msgid "jpg file extension expected."
+msgstr "L’extension attendue du fichier est jpg"
+
+#: waeup/kofa/applicants/browser.py:795 waeup/kofa/applicants/browser.py:952
+#: waeup/kofa/applicants/browser.py:1008
+msgid "Final Submit"
+msgstr "Dernière Validation"
+
+#: waeup/kofa/applicants/browser.py:796 waeup/kofa/applicants/browser.py:908
+#: waeup/kofa/applicants/browser.py:950 waeup/kofa/applicants/browser.py:985
+msgid "Add online payment ticket"
+msgstr "Ajout de ticket de paiement en ligne"
+
+#: waeup/kofa/applicants/browser.py:796 waeup/kofa/applicants/browser.py:913
+#: waeup/kofa/applicants/browser.py:950 waeup/kofa/applicants/browser.py:953
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/students/browser.py:1343
+#: waeup/kofa/students/browser.py:1391 waeup/kofa/students/browser.py:1583
+#: waeup/kofa/students/browser.py:2557
+msgid "Remove selected tickets"
+msgstr "Supprimer les tickets sélectionnés"
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:826
+#, fuzzy
+msgid "${a} <br /> Application Form ${b}"
+msgstr "${a} formulaire de demande ${b}"
+
+#: waeup/kofa/applicants/browser.py:838 waeup/kofa/students/browser.py:592
+msgid "No transition"
+msgstr "Pas de transition"
+
+#: waeup/kofa/applicants/browser.py:84
+msgid "Application Section"
+msgstr "Section demande"
+
+#: waeup/kofa/applicants/browser.py:883 waeup/kofa/students/browser.py:1589
+msgid "No payment selected."
+msgstr "Aucun paiement sélectionné."
+
+#: waeup/kofa/applicants/browser.py:961 waeup/kofa/students/browser.py:108
+msgid "The requested form is locked (read-only)."
+msgstr "Le formulaire demandé est verrouillé (lecture seule)."
+
+#: waeup/kofa/applicants/browser.py:977
+msgid "No passport picture uploaded."
+msgstr "Aucune photo de passeport enregistrée."
+
+#: waeup/kofa/applicants/browser.py:979
+msgid "Passport picture confirmation box not ticked."
+msgstr "Case de confirmation de photo de passeport non sélectionnée."
+
+#: 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:52
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:28
+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:25
+#, fuzzy
+msgid "Passport Photo:"
+msgstr "Photo d’Identité"
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:50
+msgid "Application Transition:"
+msgstr "Transition de demande:"
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:66
+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:82
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:40
+#, fuzzy
+msgid "Application Fee Payment Tickets"
+msgstr "Ticket de paiement des frais d’acceptation"
+
+#: 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/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:46
+msgid "Application Number"
+msgstr "Numéro de la demande"
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:50
+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:92
+msgid "note_acmp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:93
+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:38
+msgid "note_armp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:56
+msgid "Records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:6
+#, fuzzy
+msgid "Introduction"
+msgstr "Instructions:"
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:12
+msgid "Application Period"
+msgstr "Période de la demande"
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:50
+msgid "Year"
+msgstr "Année"
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:11
+#, fuzzy
+msgid "with email address"
+msgstr "Adresse email"
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:5
+#, fuzzy
+msgid "for applicant"
+msgstr "Ajoutez Candidat"
+
+#: waeup/kofa/applicants/export.py:39
+#, fuzzy
+msgid "Basic Applicants Containers"
+msgstr "Espace Candidat"
+
+#: waeup/kofa/applicants/interfaces.py:143
+#, fuzzy
+msgid "Create Application Records"
+msgstr "Créer les données de la demande"
+
+#: waeup/kofa/applicants/interfaces.py:144
+#, fuzzy
+msgid "Update Application Records"
+msgstr "Données de demande"
+
+#: waeup/kofa/applicants/interfaces.py:174
+#: waeup/kofa/applicants/interfaces.py:238
+#, fuzzy
+msgid "Human readable description in HTML format"
+msgstr "Description lisible dans le format reST"
+
+#: waeup/kofa/applicants/interfaces.py:192 waeup/kofa/students/browser.py:1130
+#: waeup/kofa/students/browser.py:1290 waeup/kofa/university/interfaces.py:38
+#: waeup/kofa/university/interfaces.py:66
+#: waeup/kofa/university/interfaces.py:86
+#: waeup/kofa/university/interfaces.py:117
+#: waeup/kofa/university/interfaces.py:139
+#: waeup/kofa/university/interfaces.py:188
+#: waeup/kofa/university/interfaces.py:201
+#: waeup/kofa/university/interfaces.py:311
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:39
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:68
+#: waeup/kofa/browser/templates/facultypage.pt:8
+#: waeup/kofa/browser/templates/departmentpage.pt:15
+#: waeup/kofa/browser/templates/departmentpage.pt:38
+#: waeup/kofa/browser/templates/searchpage.pt:13
+#: waeup/kofa/browser/templates/facultymanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:15
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:26
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:52
+msgid "Code"
+msgstr "Code"
+
+#: waeup/kofa/applicants/interfaces.py:198 waeup/kofa/students/browser.py:1131
+#: waeup/kofa/students/browser.py:1291 waeup/kofa/students/interfaces.py:597
+#: waeup/kofa/university/interfaces.py:208
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:70
+#: waeup/kofa/browser/templates/facultypage.pt:9
+#: waeup/kofa/browser/templates/departmentpage.pt:16
+#: waeup/kofa/browser/templates/departmentpage.pt:39
+#: waeup/kofa/browser/templates/searchpage.pt:14
+#: waeup/kofa/browser/templates/certificatepage.pt:32
+#: waeup/kofa/browser/templates/facultymanagepage.pt:43
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:38
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:37
+#: waeup/kofa/students/browser_templates/transcript.pt:38
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:16
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:27
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:54
+msgid "Title"
+msgstr "Titre"
+
+#: waeup/kofa/applicants/interfaces.py:204
+#: waeup/kofa/applicants/interfaces.py:318
+msgid "Application Target"
+msgstr "Demande Cible"
+
+#: waeup/kofa/applicants/interfaces.py:211
+#: waeup/kofa/applicants/interfaces.py:325
+msgid "Year of Entrance"
+msgstr "Année d'entrée"
+
+#: waeup/kofa/applicants/interfaces.py:218
+#, fuzzy
+msgid "Application Mode"
+msgstr "Période de la demande"
+
+#: waeup/kofa/applicants/interfaces.py:232
+msgid "Category for the grouping of certificates"
+msgstr "Catégorie pour le regroupement des certificats"
+
+#: waeup/kofa/applicants/interfaces.py:251
+msgid "Application Start Date"
+msgstr "Date du début de la demande"
+
+#: waeup/kofa/applicants/interfaces.py:253
+#: waeup/kofa/applicants/interfaces.py:259 waeup/kofa/hostels/interfaces.py:39
+#: waeup/kofa/hostels/interfaces.py:45
+msgid "Example:"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:257
+msgid "Application Closing Date"
+msgstr "Date limite de la demande"
+
+#: waeup/kofa/applicants/interfaces.py:263
+msgid "Forbid additions after deadline (enddate)"
+msgstr "Interdiction d'ajout après la date-limite (date de clôture)"
+
+#: waeup/kofa/applicants/interfaces.py:269 waeup/kofa/interfaces.py:652
+#, fuzzy
+msgid "Application Fee"
+msgstr "Demande réinitialisée"
+
+#: waeup/kofa/applicants/interfaces.py:275
+#, fuzzy
+msgid "Hide container"
+msgstr "Gérer les blocs des demandes"
+
+#: waeup/kofa/applicants/interfaces.py:353
+#: waeup/kofa/students/interfaces.py:185
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:359
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:26
+msgid "Applicant Id"
+msgstr "Id du demandeur"
+
+#: waeup/kofa/applicants/interfaces.py:364
+#: waeup/kofa/students/interfaces.py:225 waeup/kofa/students/interfaces.py:356
+msgid "Registration Number"
+msgstr "Numéro d’Inscription"
+
+#: waeup/kofa/applicants/interfaces.py:375
+#: waeup/kofa/applicants/interfaces.py:526
+#: waeup/kofa/students/interfaces.py:204 waeup/kofa/students/interfaces.py:381
+msgid "First Name"
+msgstr "Prénom"
+
+#: waeup/kofa/applicants/interfaces.py:379
+#: waeup/kofa/students/interfaces.py:209
+msgid "Middle Name"
+msgstr "Deuxième prénom"
+
+#: waeup/kofa/applicants/interfaces.py:383
+#: waeup/kofa/students/interfaces.py:214
+msgid "Last Name (Surname)"
+msgstr "Nom de famille"
+
+#: waeup/kofa/applicants/interfaces.py:387
+#: waeup/kofa/students/interfaces.py:297
+msgid "Date of Birth"
+msgstr "Date de naissance"
+
+#: waeup/kofa/applicants/interfaces.py:393
+#: waeup/kofa/students/interfaces.py:219
+msgid "Sex"
+msgstr "Sexe"
+
+#: waeup/kofa/applicants/interfaces.py:398
+#: waeup/kofa/applicants/interfaces.py:474
+#: waeup/kofa/applicants/interfaces.py:531
+#: waeup/kofa/browser/interfaces.py:221 waeup/kofa/interfaces.py:435
+#: waeup/kofa/interfaces.py:511 waeup/kofa/students/interfaces.py:386
+msgid "Email Address"
+msgstr "Adresse email"
+
+#: waeup/kofa/applicants/interfaces.py:403 waeup/kofa/interfaces.py:440
+#: waeup/kofa/interfaces.py:518 waeup/kofa/students/interfaces.py:250
+msgid "Phone"
+msgstr "Téléphone"
+
+#: waeup/kofa/applicants/interfaces.py:408
+#: waeup/kofa/applicants/interfaces.py:479
+msgid "1st Choice Course of Study"
+msgstr "Premier choix "
+
+#: waeup/kofa/applicants/interfaces.py:413
+#: waeup/kofa/applicants/interfaces.py:484
+msgid "2nd Choice Course of Study"
+msgstr "Deuxième choix "
+
+#: waeup/kofa/applicants/interfaces.py:425
+#: waeup/kofa/applicants/interfaces.py:495
+msgid "Notice"
+msgstr "Notification"
+
+#: waeup/kofa/applicants/interfaces.py:429
+#: waeup/kofa/students/interfaces.py:199
+#: waeup/kofa/browser/templates/showstudentspage.pt:4
+#: waeup/kofa/students/browser_templates/containerpage.pt:32
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:38
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:56
+msgid "Student Id"
+msgstr "Id de l'étudiant"
+
+#: waeup/kofa/applicants/interfaces.py:434
+#: waeup/kofa/applicants/interfaces.py:489
+msgid "Admitted Course of Study"
+msgstr "Admis au cours"
+
+#: waeup/kofa/applicants/interfaces.py:439
+msgid "Form locked"
+msgstr "Formulaire véroué"
+
+#: waeup/kofa/applicants/payment.py:53 waeup/kofa/applicants/workflow.py:74
+#: waeup/kofa/students/payments.py:115
+#, fuzzy
+msgid "Payment approved"
+msgstr "Date de Paiement"
+
+#: waeup/kofa/applicants/payment.py:66 waeup/kofa/students/payments.py:104
+#, fuzzy
+msgid "Successful payment"
+msgstr "Ajouté avec succès ${a}."
+
+#: waeup/kofa/applicants/payment.py:73 waeup/kofa/students/payments.py:122
+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:40
+#, fuzzy
+msgid "Desired Study Courses"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/applicants/utils.py:41
+#, fuzzy
+msgid "Process Data"
+msgstr "Données Personnels"
+
+#: waeup/kofa/applicants/utils.py:56 waeup/kofa/students/browser.py:139
+#: waeup/kofa/students/utils.py:356 waeup/kofa/students/utils.py:383
+#: waeup/kofa/students/utils.py:394 waeup/kofa/students/utils.py:450
+msgid "Session configuration object is not available."
+msgstr "L'objet de configuration de la session n'est pas valide."
+
+#: 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:146 waeup/kofa/applicants/viewlets.py:155
+#, fuzzy
+msgid "Create students"
+msgstr "Créer étudiant"
+
+#: waeup/kofa/applicants/viewlets.py:163
+msgid "View application record"
+msgstr "Aperçu des données de la demande"
+
+#: waeup/kofa/applicants/viewlets.py:171
+msgid "Manage application record"
+msgstr "Gérer les données de la demande"
+
+#: waeup/kofa/applicants/viewlets.py:179
+msgid "Edit application record"
+msgstr "Éditer les données de la demande"
+
+#: waeup/kofa/applicants/viewlets.py:197
+msgid "Download application slip"
+msgstr "Télécharger la demande en veille"
+
+#: waeup/kofa/applicants/viewlets.py:213 waeup/kofa/applicants/workflow.py:108
+#: waeup/kofa/students/browser.py:361
+msgid "Create student record"
+msgstr "Créer les données d’étudiant"
+
+#: waeup/kofa/applicants/viewlets.py:230 waeup/kofa/students/viewlets.py:480
+#, fuzzy
+msgid "Download payment slip"
+msgstr "Télécharger le reçu du paiement"
+
+#: waeup/kofa/applicants/viewlets.py:245 waeup/kofa/applicants/workflow.py:73
+#: waeup/kofa/students/viewlets.py:495 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:728
+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:91
+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:90
+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:93
+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:652
+msgid "Academics"
+msgstr "Universitaires"
+
+#: waeup/kofa/browser/breadcrumbs.py:127 waeup/kofa/browser/pages.py:460
+msgid "Administration"
+msgstr "Administration"
+
+#: waeup/kofa/browser/breadcrumbs.py:134 waeup/kofa/browser/viewlets.py:335
+#: waeup/kofa/browser/viewlets.py:703
+msgid "Portal Configuration"
+msgstr "Configuration du portail"
+
+#: waeup/kofa/browser/breadcrumbs.py:152 waeup/kofa/browser/pages.py:507
+#: waeup/kofa/browser/viewlets.py:344 waeup/kofa/browser/viewlets.py:713
+msgid "Portal Users"
+msgstr "Les utilisateurs du portail"
+
+#: waeup/kofa/browser/breadcrumbs.py:159 waeup/kofa/browser/pages.py:885
+#: waeup/kofa/browser/pages.py:1493 waeup/kofa/browser/viewlets.py:353
+#: waeup/kofa/browser/viewlets.py:722
+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:362 waeup/kofa/browser/viewlets.py:731
+#, fuzzy
+msgid "Reports"
+msgstr "Réimporter"
+
+#: waeup/kofa/browser/breadcrumbs.py:174 waeup/kofa/students/browser.py:2910
+#, fuzzy
+msgid "Student Data Exports"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/browser/breadcrumbs.py:94
+msgid "Home"
+msgstr "Accueil"
+
+#: waeup/kofa/browser/interfaces.py:212
+msgid "Unique Identifier"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:213
+msgid ""
+"User Name, Student or Applicant Id, Matriculation or Registration Number"
+msgstr ""
+
+#: waeup/kofa/browser/layout.py:97
+msgid "'Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1009
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1011 waeup/kofa/browser/pages.py:2385
+#: waeup/kofa/students/browser.py:491
+msgid "An smtp server error occurred."
+msgstr "Une erreur du serveur smtp produite."
+
+#: waeup/kofa/browser/pages.py:1019
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1033
+#, fuzzy
+msgid "Only csv files are allowed."
+msgstr "Actuellement aucun de fichier journal n’est valable"
+
+#: waeup/kofa/browser/pages.py:1041
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1052 waeup/kofa/browser/pages.py:1059
+msgid "Your file contains forbidden characters. Please replace."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1169 waeup/kofa/browser/pages.py:1199
+#: waeup/kofa/browser/pages.py:1302 waeup/kofa/browser/pages.py:1415
+msgid "Process CSV file"
+msgstr "Traitement fichier CSV"
+
+#: waeup/kofa/browser/pages.py:1183 waeup/kofa/browser/pages.py:1267
+#: waeup/kofa/browser/pages.py:1391
+msgid "Import aborted."
+msgstr "Importation échouée."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:119
+msgid "Local role successfully removed: ${a}"
+msgstr "Role local supprimé avec success: ${a}"
+
+#: waeup/kofa/browser/pages.py:1202
+msgid "Back to step 1"
+msgstr "Retour à l’étape 1"
+
+#: waeup/kofa/browser/pages.py:1203
+msgid "Proceed to step 3"
+msgstr "Passer à l’étape 3"
+
+#: waeup/kofa/browser/pages.py:1246
+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:1254
+msgid "Replace imported file!"
+msgstr "Remplacer le fichier importé!"
+
+#: waeup/kofa/browser/pages.py:1279
+msgid "Update mode only!"
+msgstr "Mettre à jour mode seulement!"
+
+#: waeup/kofa/browser/pages.py:1305 waeup/kofa/browser/pages.py:1534
+msgid "Reset"
+msgstr "Réinitialiser"
+
+#: waeup/kofa/browser/pages.py:1306
+msgid "Set headerfields"
+msgstr "Mettre l’en-tête des champ"
+
+#: waeup/kofa/browser/pages.py:1307
+msgid "Back to step 2"
+msgstr "Retour à l’étape 2"
+
+#: waeup/kofa/browser/pages.py:1308
+msgid "Perform import"
+msgstr "Effectuer l’importation"
+
+#: waeup/kofa/browser/pages.py:132 waeup/kofa/browser/pages.py:899
+#: waeup/kofa/hostels/browser.py:290 waeup/kofa/hostels/browser.py:320
+#: waeup/kofa/students/browser.py:333
+msgid "No item selected."
+msgstr "Aucun element sélectionné."
+
+#: waeup/kofa/browser/pages.py:1373
+msgid "Edit headers or replace imported file!"
+msgstr "Editer les en-têtes ou remplacer le fichier importé"
+
+#: waeup/kofa/browser/pages.py:1417
+#, fuzzy
+msgid "Process next"
+msgstr "Processeur:"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1467
+msgid "Processing of ${a} rows failed."
+msgstr "Traitement des ${a} lignes erronées."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1469
+msgid "Successfully processed ${a} rows."
+msgstr "${a} lignes traitées avec succès."
+
+#: waeup/kofa/browser/pages.py:1477
+msgid "Show logfiles"
+msgstr "Afficher les journaux "
+
+#: waeup/kofa/browser/pages.py:1479
+msgid "Back to Data Center"
+msgstr "Retour au centre des données"
+
+#: waeup/kofa/browser/pages.py:1480
+msgid "Show"
+msgstr "Afficher"
+
+#: waeup/kofa/browser/pages.py:1496
+msgid "Back"
+msgstr "Revenir"
+
+#: waeup/kofa/browser/pages.py:1497
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1505
+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:1520
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1523
+#, fuzzy
+msgid "No search results found."
+msgstr "Pas d’enregistrement d’étudiant trouvé."
+
+#: waeup/kofa/browser/pages.py:1531
+msgid "Edit data center settings"
+msgstr "Editer les paramètres du centre de données"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1551
+msgid "File already existed (not copied): ${a}"
+msgstr "Fichier déjà existant (ne pas copié): ${a}"
+
+#: waeup/kofa/browser/pages.py:1554
+msgid "Given storage path cannot be used."
+msgstr "Le Chemin de stockage donné ne peut pas être utilisé."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1555
+msgid "Error: ${a}"
+msgstr "Erreur: ${a}"
+
+#: waeup/kofa/browser/pages.py:1558
+msgid "New storage path succefully set."
+msgstr "Le nouveau chemin de stockage a été défini avec succès."
+
+#: waeup/kofa/browser/pages.py:1570
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1572
+#, fuzzy
+msgid "Create CSV file"
+msgstr "Transférer le fichier CSV"
+
+#: waeup/kofa/browser/pages.py:1597 waeup/kofa/students/browser.py:2923
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1689
+msgid "Academic Section"
+msgstr "Section Universitaire"
+
+#: waeup/kofa/browser/pages.py:1701 waeup/kofa/browser/pages.py:1720
+#: waeup/kofa/browser/pages.py:1737 waeup/kofa/browser/pages.py:1741
+msgid "Add faculty"
+msgstr "Ajouter faculté"
+
+#: waeup/kofa/browser/pages.py:1702 waeup/kofa/university/export.py:36
+msgid "Faculties"
+msgstr "Les facultés"
+
+#: waeup/kofa/browser/pages.py:1706 waeup/kofa/browser/viewlets.py:419
+msgid "Manage academic section"
+msgstr "Gestion des sections universitaires"
+
+#: waeup/kofa/browser/pages.py:1715
+#, 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:1748
+msgid "The faculty code chosen already exists."
+msgstr "Le code de faculté choisi existe déjà."
+
+#: waeup/kofa/browser/pages.py:1773 waeup/kofa/browser/pages.py:1783
+#: waeup/kofa/university/export.py:78
+msgid "Departments"
+msgstr "Départements"
+
+#: waeup/kofa/browser/pages.py:1785 waeup/kofa/browser/pages.py:1833
+#: waeup/kofa/browser/pages.py:1852 waeup/kofa/browser/pages.py:1856
+msgid "Add department"
+msgstr "Ajouter département"
+
+#: waeup/kofa/browser/pages.py:1793 waeup/kofa/browser/viewlets.py:446
+msgid "Manage faculty"
+msgstr "Gérer la faculté"
+
+#: waeup/kofa/browser/pages.py:1819
+#, 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:1863
+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:1866
+msgid "Department ${a} added."
+msgstr "Département ${a} ajouté."
+
+#: waeup/kofa/browser/pages.py:1888
+msgid "Courses and Certificates"
+msgstr "Cours et Certificats"
+
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1971
+msgid "Remove selected courses"
+msgstr "Supprimer les cours sélectionnés"
+
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1986
+#: waeup/kofa/browser/pages.py:2015 waeup/kofa/browser/pages.py:2019
+msgid "Add course"
+msgstr "Ajouter cours"
+
+#: waeup/kofa/browser/pages.py:1920 waeup/kofa/browser/pages.py:1977
+msgid "Remove selected certificates"
+msgstr "Supprimer les certificats sélectionnés"
+
+#: waeup/kofa/browser/pages.py:1920 waeup/kofa/browser/pages.py:1991
+#: waeup/kofa/browser/pages.py:2056 waeup/kofa/browser/pages.py:2060
+msgid "Add certificate"
+msgstr "Ajouter certificat"
+
+#: waeup/kofa/browser/pages.py:1929 waeup/kofa/browser/viewlets.py:453
+msgid "Manage department"
+msgstr "Gérer département"
+
+#: waeup/kofa/browser/pages.py:1980
+#, 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:2031
+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:2037
+msgid "Course ${a} successfully created."
+msgstr "Cours ${a} créé avec succès."
+
+#: waeup/kofa/browser/pages.py:2072
+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:2078
+msgid "Certificate ${a} successfully created."
+msgstr "Certificat ${a} créé avec succès."
+
+#: waeup/kofa/browser/pages.py:2113 waeup/kofa/browser/viewlets.py:483
+msgid "Edit course"
+msgstr "Editer cours"
+
+#: waeup/kofa/browser/pages.py:2155
+msgid "Edit certificate"
+msgstr "Editer certificat"
+
+#: waeup/kofa/browser/pages.py:2162 waeup/kofa/browser/pages.py:2191
+#: waeup/kofa/browser/pages.py:2228 waeup/kofa/browser/pages.py:2230
+#, fuzzy
+msgid "Add certificate course"
+msgstr "Ajouter certificat"
+
+#: waeup/kofa/browser/pages.py:2163 waeup/kofa/browser/pages.py:2186
+#, fuzzy
+msgid "Remove selected certificate courses"
+msgstr "Supprimer les certificats sélectionnés"
+
+#: waeup/kofa/browser/pages.py:2169 waeup/kofa/browser/viewlets.py:513
+msgid "Manage certificate"
+msgstr "Gérer certificact"
+
+#: waeup/kofa/browser/pages.py:2235
+#, 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:2239
+#, fuzzy
+msgid "certificate course ${a}_${b} added."
+msgstr "Référant du cours ${a}_${b} ajouté"
+
+#: waeup/kofa/browser/pages.py:2280 waeup/kofa/browser/viewlets.py:543
+#, fuzzy
+msgid "Edit certificate course"
+msgstr "Editer certificat"
+
+#: waeup/kofa/browser/pages.py:2283
+msgid "Save and return"
+msgstr "Enregistrer et revenir"
+
+#: waeup/kofa/browser/pages.py:2316
+#, fuzzy
+msgid "Send me a new password"
+msgstr "Changer mot de passe"
+
+#: waeup/kofa/browser/pages.py:2364
+#, fuzzy
+msgid "No record found."
+msgstr "Pas d’enregistrement d’étudiant trouvé."
+
+#: waeup/kofa/browser/pages.py:2378 waeup/kofa/students/browser.py:2748
+#, 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:2382
+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:246 waeup/kofa/browser/viewlets.py:281
+#: waeup/kofa/browser/templates/staffsitelayout.pt:28
+#: waeup/kofa/browser/templates/studentsitelayout.pt:32
+msgid "Login"
+msgstr "Connexion"
+
+#: waeup/kofa/browser/pages.py:257
+msgid "You logged in."
+msgstr "Vous etes connecté"
+
+#: waeup/kofa/browser/pages.py:266
+msgid "Your personal data record is outdated. Please update."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:300
+#, fuzzy
+msgid "Your account has been temporarily deactivated."
+msgstr "La liste de cours a été validée."
+
+#: waeup/kofa/browser/pages.py:307
+#, fuzzy
+msgid "Your account has been deactivated."
+msgstr "La liste de cours a été validée."
+
+#: waeup/kofa/browser/pages.py:309
+#, fuzzy
+msgid "You entered invalid credentials."
+msgstr "Vous avez entré des références erronées"
+
+#: waeup/kofa/browser/pages.py:324
+#, 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:365
+msgid "Contact ${a}"
+msgstr "Contacte ${a}"
+
+#: waeup/kofa/browser/pages.py:371 waeup/kofa/browser/pages.py:652
+msgid "Send message now"
+msgstr "Envoyer un message maintenant"
+
+#: waeup/kofa/browser/pages.py:390 waeup/kofa/browser/pages.py:423
+#: waeup/kofa/browser/pages.py:669 waeup/kofa/students/browser.py:489
+msgid "Your message has been sent."
+msgstr "Votre message a été envoyé."
+
+#: waeup/kofa/browser/pages.py:410
+msgid "Send now"
+msgstr "Envoyer maintenant"
+
+#: waeup/kofa/browser/pages.py:425
+msgid "A smtp server error occurred."
+msgstr "Une erreur de serveur smtp s’est produite."
+
+#: waeup/kofa/browser/pages.py:450
+#, fuzzy
+msgid "<h1>Welcome to WAeUP.Kofa</h1>"
+msgstr "<h1> Bienvenue à WAeUP.SIRP<h1>"
+
+#: waeup/kofa/browser/pages.py:508 waeup/kofa/hostels/browser.py:228
+#: waeup/kofa/students/viewlets.py:168 waeup/kofa/students/viewlets.py:252
+#: waeup/kofa/students/viewlets.py:321 waeup/kofa/students/viewlets.py:337
+#: waeup/kofa/students/viewlets.py:396 waeup/kofa/students/viewlets.py:463
+msgid "Manage"
+msgstr "Gérer"
+
+#: waeup/kofa/browser/pages.py:509 waeup/kofa/browser/viewlets.py:190
+msgid "Remove"
+msgstr "Supprimer"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:517
+msgid "User account ${a} successfully deleted."
+msgstr "Le compte d’utilisateur ${a} supprimé avec succès."
+
+#: waeup/kofa/browser/pages.py:554 waeup/kofa/browser/pages.py:556
+#: waeup/kofa/browser/viewlets.py:549
+msgid "Add user"
+msgstr "Ajouter utilisateur"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:578
+msgid "User account ${a} successfully added."
+msgstr "Compte utilisateur ${a} ajouté avec succès."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:599
+msgid "Edit user ${a}"
+msgstr "Editer utilisateur ${a}"
+
+#: waeup/kofa/browser/pages.py:633
+msgid "User settings have been saved."
+msgstr "Les paramètres de l’utilisateur ont été sauvegardés."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:650 waeup/kofa/students/browser.py:470
+msgid "Send message to ${a}"
+msgstr "Envoyer message à ${a}"
+
+#: waeup/kofa/browser/pages.py:679 waeup/kofa/browser/viewlets.py:369
+msgid "My Preferences"
+msgstr "Mes Préférences"
+
+#: waeup/kofa/browser/pages.py:692 waeup/kofa/browser/viewlets.py:385
+msgid "My Roles"
+msgstr "Mes Roles"
+
+#: waeup/kofa/browser/pages.py:723
+msgid "Search Academic Section"
+msgstr "Chercher section universitaire"
+
+#: waeup/kofa/browser/pages.py:750
+msgid "View portal configuration"
+msgstr "Voir la configuration du portail"
+
+#: waeup/kofa/browser/pages.py:763
+msgid "Edit portal configuration"
+msgstr "Editer la configuration du portail"
+
+#: waeup/kofa/browser/pages.py:764 waeup/kofa/browser/pages.py:814
+msgid "Update plugins"
+msgstr "Mise à jour modules"
+
+#: waeup/kofa/browser/pages.py:766 waeup/kofa/browser/pages.py:793
+#: waeup/kofa/browser/pages.py:826
+msgid "Add session configuration"
+msgstr "Ajouter configuration de session"
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:805
+msgid "Session ${a} Configuration"
+msgstr "Configuration ${a} Session"
+
+#: waeup/kofa/browser/pages.py:817
+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:830
+msgid "Add Session Configuration"
+msgstr "Ajouter Configuration de Session"
+
+#: waeup/kofa/browser/pages.py:841
+msgid "The session chosen already exists."
+msgstr "La session choisie existe déjà."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:863
+msgid "Edit academic session ${a} configuration"
+msgstr "Editer configuration ${a} session universitaire"
+
+#: waeup/kofa/browser/pages.py:910
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:913
+#, fuzzy
+msgid "Successfully deleted: ${a}"
+msgstr "Supprimer avec succès: ${a}"
+
+#: waeup/kofa/browser/pages.py:924
+#, fuzzy
+msgid "Processed Files"
+msgstr "Traitement fichier CSV"
+
+#: waeup/kofa/browser/pages.py:935
+#, fuzzy
+msgid "Upload portal data as CSV file"
+msgstr "Transférer le fichier CSV"
+
+#: waeup/kofa/browser/pages.py:938
+msgid "Upload"
+msgstr "Télécharger (vers le serveur)"
+
+#: waeup/kofa/browser/pages.py:984
+#, fuzzy
+msgid "Import Manager"
+msgstr "Importation échouée."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:986
+#, fuzzy
+msgid "${a}: ${b} uploaded"
+msgstr "Fichier ${a} chargé."
+
+#: waeup/kofa/browser/pages.py:99
+msgid "No local role selected."
+msgstr "Aucun rôle local sélectionné."
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:990
+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/pdf.py:153
+#, fuzzy
+msgid "Date:"
+msgstr "Date"
+
+#: waeup/kofa/browser/pdf.py:204 waeup/kofa/browser/pdf.py:461
+#: waeup/kofa/browser/pdf.py:471
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:12
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:16
+msgid "Date"
+msgstr "Date"
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:522
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:791
+#, 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
+#: waeup/kofa/students/browser_templates/exportconfig_course.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:6
+#, fuzzy
+msgid "Certificate Courses"
+msgstr "Certificats"
+
+#: waeup/kofa/browser/templates/certificatepage.pt:29
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:35
+#: waeup/kofa/students/browser_templates/transcript.pt:36
+msgid "Semester"
+msgstr "Semestre"
+
+#: waeup/kofa/browser/templates/certificatepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:36
+#, fuzzy
+msgid "Cert. Course"
+msgstr "Cours Echoué"
+
+#: waeup/kofa/browser/templates/certificatepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:37
+#, 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:8
+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:34
+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/datacenterexportpage.pt:44
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:4
+#, fuzzy
+msgid "Export Number"
+msgstr "Numéro "
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:45
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:5
+msgid "Exporter"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:46
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:6
+msgid "Filter"
+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 "Etape 1"
+
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr "Nom du fichier"
+
+#: 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:16
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:18
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:41
+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:24
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:13
+msgid "Processing mode:"
+msgstr "Mode de traitement:"
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:37
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:15
+msgid "Header"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:38
+#, fuzzy
+msgid "change to"
+msgstr "changer à:"
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:39
+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:2
+msgid "Batch processing finished."
+msgstr "Traitement par lots terminé."
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:14
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr "Taille"
+
+#: 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/datacenterlogspage.pt:36
+#, fuzzy
+msgid "Reload page"
+msgstr "Date du téléchargement"
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:12
+msgid "Overwrite contents in new location?"
+msgstr "Ecraser le contenu de nouvel emplacement?"
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:3
+#: waeup/kofa/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr "Chemin de stockage:"
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:7
+msgid "Copy existing data to new storage?"
+msgstr "Copier les données existantes vers un nouvel 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:18
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:14
+msgid "Datasets"
+msgstr "Ensemble des données"
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:19
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:11
+msgid "Upload Date"
+msgstr "Date du téléchargement"
+
+#: 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:113
+msgid "Download"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr "Fichier:"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:22
+msgid "Processor:"
+msgstr "Processeur:"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:35
+#, fuzzy
+msgid "Import Mode:"
+msgstr "Importation échouée."
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:59
+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:67
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:72
+#, fuzzy
+msgid "Processor"
+msgstr "Processeur:"
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:73
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:74
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:75
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:76
+msgid "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:101
+#: waeup/kofa/browser/templates/facultymanagepage.pt:75
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:97
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:112
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:91
+msgid "Local Role"
+msgstr "Rôle Local"
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:16
+#: waeup/kofa/browser/templates/userscontainerpage.pt:7
+#: waeup/kofa/browser/templates/facultymanagepage.pt:16
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:17
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:16
+msgid "Local Roles"
+msgstr "Rôles Locaux"
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:6
+#: waeup/kofa/browser/templates/facultymanagepage.pt:6
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:5
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:7
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:9
+msgid "Settings"
+msgstr "Paramètres"
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:99
+#: waeup/kofa/browser/templates/userscontainerpage.pt:5
+#: waeup/kofa/browser/templates/facultymanagepage.pt:73
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:110
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:89
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:22
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:39
+msgid "Name"
+msgstr "Nom"
+
+#: 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/fileupload.pt:26
+#: waeup/kofa/students/browser_templates/imageupload.pt:20
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:32
+msgid "Max. file size:"
+msgstr "Taille de fichier Max.:"
+
+#: waeup/kofa/browser/templates/loginpage.pt:13
+#: waeup/kofa/browser/templates/usereditformpage.pt:9
+#: waeup/kofa/students/browser_templates/basepage.pt:14
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:18
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:27
+#: waeup/kofa/students/browser_templates/change_password.pt:10
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:31
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:27
+msgid "Password:"
+msgstr "Mot de passe"
+
+#: waeup/kofa/browser/templates/loginpage.pt:24
+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:36
+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:39
+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:44
+#, 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:48
+msgid "login_trouble3"
+msgstr "Problème de connexion3"
+
+#: waeup/kofa/browser/templates/loginpage.pt:5
+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/reportscontainercreate.pt:22
+msgid "Type of report to create"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:4
+#, fuzzy
+msgid "Report Number"
+msgstr "Numéro Reg."
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:41
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:76
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:36
+msgid "<img src=\"${DYNAMIC_CONTENT}\" /> Reload"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:5
+#, fuzzy
+msgid "Description"
+msgstr "Description de Réponse"
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:51
+#, fuzzy
+msgid "Create new report"
+msgstr "Créer les données d’étudiant"
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:6
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:47
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:7
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:15
+msgid "Creator"
+msgstr "Auteur"
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:7
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:48
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:91
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:47
+msgid "Creation Date"
+msgstr "Date de Création"
+
+#: waeup/kofa/browser/templates/searchpage.pt:16
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:17
+msgid "Department"
+msgstr "Département"
+
+#: waeup/kofa/browser/templates/searchpage.pt:17
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:46
+msgid "Type"
+msgstr "Type"
+
+#: waeup/kofa/browser/templates/searchpage.pt:9
+#: waeup/kofa/students/browser_templates/containerpage.pt:24
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:18
+msgid "Search Results"
+msgstr "Résultats de la Recherche"
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:10
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:62
+msgid "Comment"
+msgstr ""
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:5
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:57
+msgid "Fullname"
+msgstr "Nom complet"
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:6
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:8
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:49
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:58
+msgid "Status"
+msgstr "Statut"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:113
+#: waeup/kofa/browser/templates/studentsitelayout.pt:97
+#, fuzzy
+msgid "Back to bottom"
+msgstr "Retour au début"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:122
+#: waeup/kofa/browser/templates/studentsitelayout.pt:107
+msgid "Back to top"
+msgstr "Retour au début"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:124
+#: waeup/kofa/browser/templates/studentsitelayout.pt:109
+msgid "Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:127
+#: waeup/kofa/browser/templates/studentsitelayout.pt:112
+msgid "Copyright &copy; WAeUP Group 2008-2013"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:34
+#: waeup/kofa/browser/templates/studentsitelayout.pt:38
+msgid "Contact"
+msgstr "Contact"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:4
+#: waeup/kofa/browser/templates/studentsitelayout.pt:4
+msgid "WAeUP.Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:40
+#: waeup/kofa/browser/templates/studentsitelayout.pt:44
+msgid "Logout"
+msgstr "Déconnexion"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:62
+msgid "Student Id:"
+msgstr "Id de l’Etudiant"
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:66
+msgid "State:"
+msgstr "Etat:"
+
+#: waeup/kofa/browser/templates/usereditformpage.pt:15
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:24
+#: waeup/kofa/students/browser_templates/change_password.pt:16
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:44
+#, fuzzy
+msgid "Retype Password:"
+msgstr "Resaisir le mot de passe"
+
+#: waeup/kofa/browser/viewlets.py:180
+msgid "Add"
+msgstr "Ajouter"
+
+#: waeup/kofa/browser/viewlets.py:320
+msgid "Any link text"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:402 waeup/kofa/students/viewlets.py:160
+msgid "Send email"
+msgstr "Envoyer un email"
+
+#: waeup/kofa/browser/viewlets.py:411
+msgid "Edit settings"
+msgstr "Editer les Paramètres"
+
+#: waeup/kofa/browser/viewlets.py:428 waeup/kofa/browser/viewlets.py:474
+#: waeup/kofa/browser/viewlets.py:504 waeup/kofa/browser/viewlets.py:534
+#, fuzzy
+msgid "Export student data"
+msgstr "pour les étudiants"
+
+#: waeup/kofa/browser/viewlets.py:437
+msgid "Search academic section"
+msgstr "Rechercher la section universitaire"
+
+#: waeup/kofa/browser/viewlets.py:463 waeup/kofa/browser/viewlets.py:493
+#: waeup/kofa/browser/viewlets.py:523
+msgid "Show students"
+msgstr "Montrer les étudiants"
+
+#: waeup/kofa/browser/viewlets.py:558
+#, fuzzy
+msgid "Show logs"
+msgstr "Afficher les journaux "
+
+#: waeup/kofa/browser/viewlets.py:567
+#, fuzzy
+msgid "Upload data"
+msgstr "Date du téléchargement"
+
+#: waeup/kofa/browser/viewlets.py:576
+#, fuzzy
+msgid "Process data"
+msgstr "Données Personnels"
+
+#: waeup/kofa/browser/viewlets.py:585
+msgid "Export data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:594
+#, fuzzy
+msgid "View processed files"
+msgstr "Consulter le journal de traitement"
+
+#: waeup/kofa/browser/viewlets.py:667
+msgid "Enquiries"
+msgstr "Requêtes"
+
+#: waeup/kofa/browser/viewlets.py:95 waeup/kofa/browser/viewlets.py:170
+#: waeup/kofa/students/viewlets.py:329 waeup/kofa/students/viewlets.py:536
+#: waeup/kofa/students/viewlets.py:583
+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:163
+#: waeup/kofa/hostels/browser.py:190
+msgid "Add hostel"
+msgstr "Ajouter dortoir"
+
+#: waeup/kofa/hostels/browser.py:133 waeup/kofa/hostels/browser.py:168
+#, fuzzy
+msgid "Clear all hostels"
+msgstr "Créer dortoir"
+
+#: waeup/kofa/hostels/browser.py:171
+#, fuzzy
+msgid "All hostels cleared."
+msgstr "Dortoir créé."
+
+#: waeup/kofa/hostels/browser.py:179
+msgid "Settings have been saved."
+msgstr "Les paramètres ont été enregistrés."
+
+#: waeup/kofa/hostels/browser.py:193
+msgid "Create hostel"
+msgstr "Créer dortoir"
+
+#: waeup/kofa/hostels/browser.py:202
+msgid "The hostel already exists."
+msgstr "Ce dortoir existe déjà."
+
+#: waeup/kofa/hostels/browser.py:204
+msgid "Hostel created."
+msgstr "Dortoir créé."
+
+#: waeup/kofa/hostels/browser.py:239
+msgid "Manage hostel"
+msgstr "Gérer dortoir"
+
+#: waeup/kofa/hostels/browser.py:242 waeup/kofa/hostels/browser.py:270
+msgid "Update all beds"
+msgstr "Mettre à jour tous les lits"
+
+#: waeup/kofa/hostels/browser.py:243 waeup/kofa/hostels/browser.py:284
+msgid "Switch reservation of selected beds"
+msgstr "Echanger la réservation des lits sélectionnés"
+
+#: waeup/kofa/hostels/browser.py:244 waeup/kofa/hostels/browser.py:314
+msgid "Release selected beds"
+msgstr "Libérer les lits sélectionnés"
+
+#: waeup/kofa/hostels/browser.py:245 waeup/kofa/hostels/browser.py:341
+#, fuzzy
+msgid "Clear hostel"
+msgstr "Créer dortoir"
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:275
+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:308
+msgid "Successfully switched beds: ${a}"
+msgstr "Lits échangés avec succès: ${a}"
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:332
+msgid "Successfully released beds: ${a}"
+msgstr "Lits libérés avec succès: ${a}"
+
+#: waeup/kofa/hostels/browser.py:337
+msgid "No allocated bed selected."
+msgstr "Pas de lit attributé sélectionné."
+
+#: waeup/kofa/hostels/browser.py:344
+#, fuzzy
+msgid "Hostel cleared."
+msgstr "Dortoir créé."
+
+#: waeup/kofa/hostels/browser.py:357
+msgid "Allocate student"
+msgstr "Allouer étudiant"
+
+#: waeup/kofa/hostels/browser.py:363
+#, 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/containerpage.pt:21
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:37
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:45
+msgid "Id"
+msgstr "Id"
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:23
+#, fuzzy
+msgid "Booked Beds"
+msgstr "Lit B"
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:47
+msgid "Number"
+msgstr "Numéro "
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:6
+msgid "Hostel Data"
+msgstr "Données Dortoir"
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:66
+msgid "[allocate student]"
+msgstr "[allouer étudiant]"
+
+#: waeup/kofa/hostels/browser_templates/hostelpage.pt:5
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:19
+#, fuzzy
+msgid "Reserved Beds:"
+msgstr "Lits réservés"
+
+#: waeup/kofa/hostels/export.py:61
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:9
+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:105
+msgid "Floors per Block"
+msgstr "Plancher par Bloque"
+
+#: waeup/kofa/hostels/interfaces.py:111
+msgid "Rooms per Floor"
+msgstr "Chambre par Plancher"
+
+#: waeup/kofa/hostels/interfaces.py:117
+msgid "Reserved Beds"
+msgstr "Lits réservés"
+
+#: waeup/kofa/hostels/interfaces.py:128
+msgid "Blocks for Female Students"
+msgstr "Bloques des Etudiantes filles"
+
+#: waeup/kofa/hostels/interfaces.py:135
+msgid "Blocks for Male Students"
+msgstr "Boques des Etudiants garçons"
+
+#: waeup/kofa/hostels/interfaces.py:142
+#, fuzzy
+msgid "Beds for Pre-Study Students"
+msgstr "Lits pour les nouveaux Etudiant"
+
+#: waeup/kofa/hostels/interfaces.py:149
+msgid "Beds for Fresh Students"
+msgstr "Lits pour les nouveaux Etudiant"
+
+#: waeup/kofa/hostels/interfaces.py:156
+msgid "Beds for Returning Students"
+msgstr "Lits pour les anciens étudiant"
+
+#: waeup/kofa/hostels/interfaces.py:163
+msgid "Beds for Final Year Students"
+msgstr "Lits pour les étudiants de dernière année"
+
+#: waeup/kofa/hostels/interfaces.py:170
+msgid "Beds without category"
+msgstr "Lits sans catégorie"
+
+#: waeup/kofa/hostels/interfaces.py:177
+msgid "Special Handling"
+msgstr "Détenu spécial"
+
+#: waeup/kofa/hostels/interfaces.py:188
+msgid "Female and male blocks overlap."
+msgstr "Bloques des filles et des garçons se chévochent"
+
+#: waeup/kofa/hostels/interfaces.py:198
+msgid "Bed categories overlap."
+msgstr "Catégories de lit se chévochent."
+
+#: waeup/kofa/hostels/interfaces.py:220
+msgid "Bed Id"
+msgstr "Id lit"
+
+#: waeup/kofa/hostels/interfaces.py:226
+msgid "Bed Type"
+msgstr "Type de lit"
+
+#: waeup/kofa/hostels/interfaces.py:232
+msgid "Bed Number"
+msgstr "Numéro du lit"
+
+#: waeup/kofa/hostels/interfaces.py:237
+msgid "Owner (Student)"
+msgstr "Propriétaire (Etudiants)"
+
+#: waeup/kofa/hostels/interfaces.py:238
+#, fuzzy
+msgid "Enter valid student id."
+msgstr "Vous avez entré des références erronées"
+
+#: waeup/kofa/hostels/interfaces.py:253
+#, 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:260
+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:93
+msgid "Sort Id"
+msgstr "Trier Id"
+
+#: waeup/kofa/hostels/interfaces.py:99
+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
+msgid "Block K"
+msgstr "Bloque K"
+
+#: waeup/kofa/hostels/vocabularies.py:63
+msgid "Block L"
+msgstr "Bloque L"
+
+#: waeup/kofa/hostels/vocabularies.py:64
+msgid "Block M"
+msgstr "Bloque M"
+
+#: waeup/kofa/hostels/vocabularies.py:65
+msgid "Block N"
+msgstr "Bloque N"
+
+#: waeup/kofa/hostels/vocabularies.py:66
+msgid "Block O"
+msgstr "Bloque O"
+
+#: waeup/kofa/hostels/vocabularies.py:67
+msgid "Block P"
+msgstr "Bloque P"
+
+#: waeup/kofa/hostels/vocabularies.py:68
+#, fuzzy
+msgid "Block Q"
+msgstr "Bloque A"
+
+#: waeup/kofa/hostels/vocabularies.py:69
+#, fuzzy
+msgid "Block R"
+msgstr "Bloque A"
+
+#: waeup/kofa/hostels/vocabularies.py:70
+#, fuzzy
+msgid "Block S"
+msgstr "Bloque A"
+
+#: waeup/kofa/hostels/vocabularies.py:71
+#, fuzzy
+msgid "Block T"
+msgstr "Bloque A"
+
+#: waeup/kofa/hostels/vocabularies.py:72
+#, fuzzy
+msgid "Block U"
+msgstr "Bloque A"
+
+#: waeup/kofa/hostels/vocabularies.py:73
+#, fuzzy
+msgid "Block V"
+msgstr "Bloque A"
+
+#: waeup/kofa/hostels/vocabularies.py:74
+#, fuzzy
+msgid "Block W"
+msgstr "Bloque A"
+
+#: waeup/kofa/interfaces.py:207
+msgid "Subject"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:211 waeup/kofa/students/browser.py:1134
+#: waeup/kofa/students/browser.py:1297
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:10
+msgid "Grade"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:368
+#, fuzzy
+msgid "Processor name"
+msgstr "Processeur:"
+
+#: waeup/kofa/interfaces.py:394
+#, fuzzy
+msgid "Email Address:"
+msgstr "Adresse email"
+
+#: waeup/kofa/interfaces.py:401
+#, fuzzy
+msgid "Email to:"
+msgstr "Email"
+
+#: waeup/kofa/interfaces.py:408
+msgid "Subject:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:412
+#, fuzzy
+msgid "Full Name:"
+msgstr "Nom Complet"
+
+#: waeup/kofa/interfaces.py:416
+msgid "Text:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:445 waeup/kofa/interfaces.py:501
+#, fuzzy
+msgid "Public Name"
+msgstr "Nom Complet"
+
+#: waeup/kofa/interfaces.py:455 waeup/kofa/interfaces.py:456
+#, fuzzy
+msgid "Number of failed logins"
+msgstr "Nombre des codes d'accès"
+
+#: waeup/kofa/interfaces.py:462
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:463
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:492
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:97
+#: waeup/kofa/browser/templates/userscontainerpage.pt:4
+#: waeup/kofa/browser/templates/facultymanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:93
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:108
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:87
+msgid "User Id"
+msgstr "Id de l’utilisateur"
+
+#: waeup/kofa/interfaces.py:497
+#: waeup/kofa/students/browser_templates/containerpage.pt:36
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:42
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:48
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:29
+msgid "Full Name"
+msgstr "Nom Complet"
+
+#: waeup/kofa/interfaces.py:507
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:524
+#: waeup/kofa/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr "Rôles Portail"
+
+#: waeup/kofa/interfaces.py:571
+#, fuzzy
+msgid "Name of University"
+msgstr "Nom du département"
+
+#: waeup/kofa/interfaces.py:572
+msgid "Sample University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:577
+msgid "Abbreviated Title of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:583
+msgid "Skin"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:59
+msgid "new"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:590
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:60
+msgid "completed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:602
+#, fuzzy
+msgid "Name of Administrator"
+msgstr "Administration"
+
+#: waeup/kofa/interfaces.py:608
+#, fuzzy
+msgid "Email Address of Administrator"
+msgstr "Adresse email"
+
+#: waeup/kofa/interfaces.py:61
+msgid "pending"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:615
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:616
+#, fuzzy
+msgid "Kofa Contact"
+msgstr "Contact"
+
+#: waeup/kofa/interfaces.py:62
+msgid "active"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:621
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:628
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:63
+msgid "assigned"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:635
+#, fuzzy
+msgid "Carry-over Course Registration"
+msgstr "Démarrer l’inscription de cours"
+
+#: waeup/kofa/interfaces.py:64
+#, fuzzy
+msgid "callbacks"
+msgstr "Demander un rappel"
+
+#: waeup/kofa/interfaces.py:644 waeup/kofa/interfaces.py:690
+#, fuzzy
+msgid "Academic Session"
+msgstr "Section Universitaire"
+
+#: waeup/kofa/interfaces.py:658
+msgid "Acceptance Fee"
+msgstr "Frais d’Acceptation"
+
+#: waeup/kofa/interfaces.py:664
+#, fuzzy
+msgid "Bed Booking Fee"
+msgstr "Date de Réservation"
+
+#: waeup/kofa/interfaces.py:670
+msgid "Rent"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:676
+#, fuzzy
+msgid "Clearance enabled"
+msgstr "acquittement démarré"
+
+#: waeup/kofa/interfaces.py:92
+msgid "clearance started"
+msgstr "acquittement démarré"
+
+#: waeup/kofa/interfaces.py:93
+msgid "clearance requested"
+msgstr "acquittement demandé"
+
+#: waeup/kofa/interfaces.py:94
+msgid "cleared"
+msgstr "acquitté"
+
+#: waeup/kofa/interfaces.py:95
+msgid "school fee paid"
+msgstr "Frais de scolarité payés"
+
+#: waeup/kofa/interfaces.py:96
+msgid "courses registered"
+msgstr "cours enregistrés"
+
+#: waeup/kofa/interfaces.py:97
+msgid "courses validated"
+msgstr "cours validés"
+
+#: waeup/kofa/interfaces.py:98
+msgid "returning"
+msgstr "Ancien (ne)"
+
+#: 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. Proceed to the login page and enter your "
+"credentials."
+msgstr ""
+"Le mot de passe a été déjà saisi. Vous utilisez le mauvais Code d’Accès."
+
+#: waeup/kofa/objecthistory.py:74
+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:782
+#: waeup/kofa/students/interfaces.py:806
+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:789
+#: waeup/kofa/students/interfaces.py:813
+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/applicants/browser_templates/applicanteditpage.pt:92
+#: 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:853
+msgid "System"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1024
+msgid "You must select a session for the level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1033
+msgid "Study level has been added."
+msgstr "Le niveau d’étude a été ajouté."
+
+#: waeup/kofa/students/browser.py:1035 waeup/kofa/students/browser.py:2477
+msgid "This level exists."
+msgstr "Ce niveau existe."
+
+#: waeup/kofa/students/browser.py:1045
+msgid "No study level selected."
+msgstr "Aucun niveau d’étude sélectionné."
+
+#: waeup/kofa/students/browser.py:1073
+#, 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:1088
+#, fuzzy
+msgid "${a}: Transcript Data"
+msgstr "${a}: Donnée de d’acquittement"
+
+#: waeup/kofa/students/browser.py:1106
+#, 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:1121 waeup/kofa/students/viewlets.py:366
+#, fuzzy
+msgid "Academic Transcript"
+msgstr "Universitaires"
+
+#: waeup/kofa/students/browser.py:1125
+msgid "CERTIFIED TRUE COPY"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1129 waeup/kofa/students/browser.py:1289
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:35
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:25
+msgid "Sem."
+msgstr "Sem."
+
+#: waeup/kofa/students/browser.py:1132 waeup/kofa/students/interfaces.py:623
+#: waeup/kofa/university/interfaces.py:152
+#: 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:1133 waeup/kofa/students/browser.py:1296
+#: waeup/kofa/students/interfaces.py:634 waeup/kofa/students/interfaces.py:667
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:20
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:32
+msgid "Score"
+msgstr "Score"
+
+#: waeup/kofa/students/browser.py:1161 waeup/kofa/students/viewlets.py:203
+#, fuzzy
+msgid "Transfer student"
+msgstr "Créer étudiant"
+
+#: waeup/kofa/students/browser.py:1171
+msgid "Transfer"
+msgstr "Transfère"
+
+#: waeup/kofa/students/browser.py:1175
+#, 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:1177
+#, fuzzy
+msgid "Former study course record incomplete."
+msgstr "Vos données sont incompletes."
+
+#: waeup/kofa/students/browser.py:1179
+msgid "Maximum number of transfers exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1181
+#, fuzzy
+msgid "Successfully transferred."
+msgstr "Ajouté avec succès ${a}."
+
+#: waeup/kofa/students/browser.py:1191
+msgid "Revert previous transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1196
+msgid "No previous transfer."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1201
+#, fuzzy
+msgid "Revert now"
+msgstr "Démarrer l’acquittement maitenant"
+
+#: waeup/kofa/students/browser.py:1204
+msgid "Previous transfer reverted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1235
+msgid "${a}: Study Level ${b}"
+msgstr "${a}: Niveau d’Etude ${b}"
+
+#: waeup/kofa/students/browser.py:1256
+msgid "Level Data"
+msgstr "Données Niveau"
+
+#: waeup/kofa/students/browser.py:1262
+#, fuzzy
+msgid "1st Semester Courses"
+msgstr "Enregistrer cours"
+
+#: waeup/kofa/students/browser.py:1268
+#, fuzzy
+msgid "2nd Semester Courses"
+msgstr "Enregistrer cours"
+
+#: waeup/kofa/students/browser.py:1274
+#, fuzzy
+msgid "Level Courses"
+msgstr "Code du Niveau"
+
+#: waeup/kofa/students/browser.py:1283
+#, fuzzy
+msgid "Course Registration Slip"
+msgstr "Imprimé des cours d' inscription"
+
+#: waeup/kofa/students/browser.py:1292
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:38
+msgid "Dept."
+msgstr "Dépt."
+
+#: waeup/kofa/students/browser.py:1293
+#: waeup/kofa/browser/templates/searchpage.pt:15
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:22
+msgid "Faculty"
+msgstr "Faculté"
+
+#: waeup/kofa/students/browser.py:1294
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:40
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:30
+msgid "Cred."
+msgstr "Créd."
+
+#: waeup/kofa/students/browser.py:1342 waeup/kofa/students/browser.py:1481
+#: waeup/kofa/students/browser.py:1492 waeup/kofa/students/browser.py:2602
+msgid "Add course ticket"
+msgstr "Ajouter ticket de cours"
+
+#: waeup/kofa/students/browser.py:1360 waeup/kofa/students/browser.py:2507
+msgid "No valid course code entered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1366 waeup/kofa/students/browser.py:2512
+#, fuzzy
+msgid "Course not found."
+msgstr "Aucun étudiant trouvé."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1383
+msgid "Manage study level ${a}"
+msgstr "Gérer niveau étude ${a}"
+
+#: waeup/kofa/students/browser.py:1397 waeup/kofa/students/browser.py:2536
+msgid "No ticket selected."
+msgstr "Aucun ticket sélectionné."
+
+#: waeup/kofa/students/browser.py:141
+msgid "Clearance is disabled for this session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1427 waeup/kofa/students/browser.py:1452
+msgid "This level does not correspond current level."
+msgstr "Ce niveau ne correspond pas au niveau actuel."
+
+#: waeup/kofa/students/browser.py:1431
+msgid "Course list has been validated."
+msgstr "La liste de cours a été validée."
+
+#: waeup/kofa/students/browser.py:1433 waeup/kofa/students/browser.py:1464
+msgid "Student is in the wrong state."
+msgstr "L’étudiant est dans le faux état."
+
+#: waeup/kofa/students/browser.py:1457
+msgid "Course list request has been annulled."
+msgstr "Demande de liste de cours a été annulée."
+
+#: waeup/kofa/students/browser.py:1461
+msgid "Course list request has been rejected:"
+msgstr "Demande de liste de cours a été rejetée:"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1516
+msgid "${a}: Course Ticket ${b}"
+msgstr "${a}: Ticket de cours ${b}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:152
+#, fuzzy
+msgid "Total credits exceed ${a}."
+msgstr "Crédits Total:"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1541
+msgid "Manage course ticket ${a}"
+msgstr "Gérer ticket de cours ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1574
+msgid "${a}: Payments"
+msgstr "${a}: Paiements"
+
+#: waeup/kofa/students/browser.py:159
+msgid "The ticket exists."
+msgstr "Ce ticket existe."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:161
+msgid "Successfully added ${a}."
+msgstr "Ajouté avec succès ${a}."
+
+#: waeup/kofa/students/browser.py:1622
+msgid "Add online payment"
+msgstr "Ajouter paiement en ligne"
+
+#: waeup/kofa/students/browser.py:1630 waeup/kofa/students/browser.py:1680
+#: waeup/kofa/students/browser.py:1711
+msgid "Create ticket"
+msgstr "Créer ticket"
+
+#: waeup/kofa/students/browser.py:1640 waeup/kofa/students/browser.py:1931
+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:1646
+#, fuzzy
+msgid "You have not yet booked accommodation."
+msgstr "Réserver chambre"
+
+#: waeup/kofa/students/browser.py:1670
+#, fuzzy
+msgid "Add previous session online payment"
+msgstr "Ajouter paiement en ligne"
+
+#: waeup/kofa/students/browser.py:1675
+msgid "No previous payment to be made."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1708
+#, fuzzy
+msgid "Add balance"
+msgstr "Ajoutez Candidat"
+
+#: waeup/kofa/students/browser.py:182 waeup/kofa/students/export.py:197
+#: waeup/kofa/students/reports.py:154 waeup/kofa/students/viewlets.py:688
+msgid "Students"
+msgstr "Etudiants"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1844
+msgid "${a}: Accommodation"
+msgstr "${a}: Hébergement"
+
+#: waeup/kofa/students/browser.py:1856
+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:1863
+msgid "No bed ticket selected."
+msgstr "Pas de ticket de lit Sélectionné."
+
+#: waeup/kofa/students/browser.py:1895
+msgid "Add bed ticket"
+msgstr "Ajouter ticket de lit"
+
+#: waeup/kofa/students/browser.py:1897
+msgid "Create bed ticket"
+msgstr "Créer ticket de lit"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1914
+msgid "Outside booking period: ${a} - ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1917
+msgid "Outside booking period."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1921
+msgid "Your data are incomplete."
+msgstr "Vos données sont incompletes."
+
+#: waeup/kofa/students/browser.py:1925
+msgid "You are in the wrong registration state."
+msgstr "Vous etes dans l’état d’enregistrement erroné."
+
+#: waeup/kofa/students/browser.py:1936
+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:1949 waeup/kofa/students/browser.py:2268
+#: waeup/kofa/students/browser.py:2410
+msgid "Activation code is invalid."
+msgstr "Le code d’activation est invalid."
+
+#: waeup/kofa/students/browser.py:1961 waeup/kofa/students/browser.py:1976
+msgid "System error: Please contact the adminsitrator."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1981 waeup/kofa/students/browser.py:2117
+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:1987 waeup/kofa/students/browser.py:2271
+#: waeup/kofa/students/browser.py:2414
+msgid "Activation code has already been used."
+msgstr "Le code d’activation a été déjà utilisé."
+
+#: waeup/kofa/students/browser.py:1990 waeup/kofa/students/browser.py:2275
+#: waeup/kofa/students/browser.py:2417 waeup/kofa/students/browser.py:2662
+msgid "invalidated"
+msgstr "invalidé"
+
+#: waeup/kofa/students/browser.py:1995 waeup/kofa/students/browser.py:2279
+#: waeup/kofa/students/browser.py:2422
+msgid "You are not the owner of this access code."
+msgstr "Vous n’etes pas le propriétaire de ce code d’accès."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2007 waeup/kofa/students/browser.py:2131
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr "${a}, Bloque ${b}, Chambre ${c}, Lit ${d} (${e})"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2016
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr "Ticket de lit créé et lit reservé: ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2033
+msgid "Bed Ticket for Session ${a}"
+msgstr "Ticket de lit pour la session ${a}"
+
+#: waeup/kofa/students/browser.py:2052
+msgid "Bed Allocation Data"
+msgstr "Données d’allocation de lit"
+
+#: waeup/kofa/students/browser.py:206 waeup/kofa/students/viewlets.py:119
+#: waeup/kofa/students/viewlets.py:751
+msgid "Study Course"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/students/browser.py:2061
+#, fuzzy
+msgid "Bed Allocation Slip"
+msgstr "Allocation Lit:"
+
+#: waeup/kofa/students/browser.py:208
+#, fuzzy
+msgid "Previous Study Course"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/students/browser.py:2089
+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:2095
+msgid "Student can't be relocated."
+msgstr "Etudiant ne peut pas être déplacé."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2139
+msgid "Student relocated: ${a}"
+msgstr "Etudiant déplacé: ${a}"
+
+#: waeup/kofa/students/browser.py:214 waeup/kofa/students/viewlets.py:125
+#: waeup/kofa/students/viewlets.py:752
+msgid "Payments"
+msgstr "Paiements"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2158
+msgid "${a}: History"
+msgstr "${a}: Histoire"
+
+#: waeup/kofa/students/browser.py:2170
+msgid "Edit base data"
+msgstr "Editer données de base"
+
+#: waeup/kofa/students/browser.py:2185 waeup/kofa/students/viewlets.py:545
+msgid "Change password"
+msgstr "Changer mot de passe"
+
+#: waeup/kofa/students/browser.py:2199
+msgid "Password changed."
+msgstr "Mot de passé changé."
+
+#: waeup/kofa/students/browser.py:2211
+msgid "Upload portrait"
+msgstr "Charger portrait"
+
+#: waeup/kofa/students/browser.py:2226 waeup/kofa/students/viewlets.py:569
+#: waeup/kofa/students/workflow.py:48
+msgid "Start clearance"
+msgstr "Démarrer l’acquittement"
+
+#: waeup/kofa/students/browser.py:2230
+msgid "Start clearance now"
+msgstr "Démarrer l’acquittement maitenant"
+
+#: waeup/kofa/students/browser.py:2248
+msgid "Wrong state"
+msgstr "Mauvais état"
+
+#: waeup/kofa/students/browser.py:2252
+msgid "No portrait uploaded."
+msgstr "Aucun portrait chargé."
+
+#: waeup/kofa/students/browser.py:2256
+msgid "Not all required fields filled."
+msgstr "Pas tous les champs demandés qui sont remplis."
+
+#: waeup/kofa/students/browser.py:2283
+msgid "Clearance process has been started."
+msgstr "Le traitement d’acquittement a été démarré."
+
+#: waeup/kofa/students/browser.py:229 waeup/kofa/students/viewlets.py:131
+msgid "Accommodation"
+msgstr "Hébergement"
+
+#: waeup/kofa/students/browser.py:2293
+msgid "Edit clearance data"
+msgstr "Editer les données d’acquittement"
+
+#: waeup/kofa/students/browser.py:2314 waeup/kofa/students/browser.py:2328
+msgid "Clearance form has been saved."
+msgstr "Le formulaire d’acquittement a été sauvegardé"
+
+#: waeup/kofa/students/browser.py:2322
+msgid "Save and request clearance"
+msgstr "Sauvegarder et demander l’acquittement"
+
+#: waeup/kofa/students/browser.py:2339 waeup/kofa/students/browser.py:2373
+msgid "This form cannot be submitted. Wrong state!"
+msgstr "Ce formulaire ne peut pas être validé. État incorrect!"
+
+#: waeup/kofa/students/browser.py:2342 waeup/kofa/students/browser.py:2376
+msgid "Clearance has been requested."
+msgstr "Acquittement a été demandé."
+
+#: waeup/kofa/students/browser.py:2351 waeup/kofa/students/workflow.py:62
+msgid "Request clearance"
+msgstr "Demander l’acquittement"
+
+#: waeup/kofa/students/browser.py:2352
+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:2355
+msgid "Request clearance now"
+msgstr "Demander l’acquittement maintenant"
+
+#: waeup/kofa/students/browser.py:2367
+msgid "This isn't your CLR access code."
+msgstr "Ce n’est pas votre code d’accès CLR."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:238
+msgid "Bed Ticket ${a}"
+msgstr "Ticket de lit ${a}"
+
+#: waeup/kofa/students/browser.py:2385
+#, fuzzy
+msgid "Start session"
+msgstr "Session"
+
+#: waeup/kofa/students/browser.py:2389
+#, fuzzy
+msgid "Start now"
+msgstr "Démarrer l’acquittement maitenant"
+
+#: waeup/kofa/students/browser.py:2398
+#, fuzzy
+msgid "You are not entitled to start session."
+msgstr "Vous etes dans l’état d’enregistrement erroné."
+
+#: waeup/kofa/students/browser.py:2435
+msgid "An error occurred, please contact the system administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2437
+#, fuzzy
+msgid "Session started."
+msgstr "Demande démarrée"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2456
+msgid "Add current level ${a}"
+msgstr "Ajouter le niveau actuel ${a}"
+
+#: waeup/kofa/students/browser.py:2468
+msgid "Create course list now"
+msgstr "Créer la liste de cours maintenant"
+
+#: waeup/kofa/students/browser.py:2479
+#, fuzzy
+msgid "Your data are incomplete"
+msgstr "Vos données sont incompletes."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2524
+#, fuzzy
+msgid "Edit course list of ${a}"
+msgstr "Editer utilisateur ${a}"
+
+#: waeup/kofa/students/browser.py:2565
+msgid "You are a postgraduate student, your course list can't bee registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:257
+msgid "Student Section"
+msgstr "Section Etudiants"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2573
+msgid "Maximum credits of ${a} exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2578
+msgid "Course list has been registered."
+msgstr "La liste de cours a été enregistrée."
+
+#: waeup/kofa/students/browser.py:2582
+msgid "Register course list"
+msgstr "Registre de liste de cours"
+
+#: waeup/kofa/students/browser.py:2618
+msgid "Set password for first-time login"
+msgstr "Mettre un mot de passe pour la première connexion"
+
+#: waeup/kofa/students/browser.py:2621
+msgid "Set"
+msgstr "Mettre"
+
+#: waeup/kofa/students/browser.py:2636
+msgid "More than one student found."
+msgstr "Plus d’un étudiant trouvé."
+
+#: waeup/kofa/students/browser.py:2644
+msgid "Access code is invalid."
+msgstr "Le code d’accès est invalide."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2647
+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:2653
+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:2659
+msgid "Access code has already been used."
+msgstr "Le code d’accès a été déjà utilisé."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2668
+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:2680
+#, fuzzy
+msgid "Request password for first-time login"
+msgstr "Mettre un mot de passe pour la première connexion"
+
+#: waeup/kofa/students/browser.py:2723 waeup/kofa/students/browser.py:2734
+#, fuzzy
+msgid "No student record found."
+msgstr "Aucun étudiant trouvé."
+
+#: waeup/kofa/students/browser.py:2768
+#, fuzzy
+msgid "Your password request was successful."
+msgstr "Votre inscription a été enregistrée avec succès."
+
+#: waeup/kofa/students/browser.py:2784 waeup/kofa/students/interfaces.py:417
+#: waeup/kofa/students/interfaces.py:508
+#: waeup/kofa/browser/templates/showstudentspage.pt:8
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:60
+msgid "Current Session"
+msgstr "Session Actuelle"
+
+#: waeup/kofa/students/browser.py:2785 waeup/kofa/students/interfaces.py:424
+#: waeup/kofa/students/interfaces.py:456 waeup/kofa/students/interfaces.py:514
+#: waeup/kofa/browser/templates/showstudentspage.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:61
+msgid "Current Level"
+msgstr "Niveau Actuel"
+
+#: waeup/kofa/students/browser.py:282
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2836
+#, fuzzy
+msgid "in any session"
+msgstr "en session"
+
+#: waeup/kofa/students/browser.py:2842
+#, fuzzy
+msgid "at any level"
+msgstr "Niveau de démarrage"
+
+#: waeup/kofa/students/browser.py:287 waeup/kofa/students/browser.py:331
+#: waeup/kofa/students/browser.py:2633 waeup/kofa/students/browser.py:2845
+msgid "No student found."
+msgstr "Aucun étudiant trouvé."
+
+#: waeup/kofa/students/browser.py:2878 waeup/kofa/students/interfaces.py:539
+#: waeup/kofa/students/interfaces.py:714 waeup/kofa/students/utils.py:224
+#: waeup/kofa/students/browser_templates/transcript.pt:35
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:8
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:14
+msgid "Session"
+msgstr "Session"
+
+#: waeup/kofa/students/browser.py:2879 waeup/kofa/students/utils.py:226
+#: waeup/kofa/university/interfaces.py:340
+#: waeup/kofa/browser/templates/certificatepage.pt:28
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:34
+#: waeup/kofa/students/browser_templates/transcript.pt:34
+msgid "Level"
+msgstr "Niveau"
+
+#: waeup/kofa/students/browser.py:2936
+#, fuzzy
+msgid "Configure student data export"
+msgstr "Créer les données d’étudiant"
+
+#: waeup/kofa/students/browser.py:2944
+#, fuzzy
+msgid "All Sessions"
+msgstr "Session"
+
+#: waeup/kofa/students/browser.py:2951
+#, fuzzy
+msgid "All Levels"
+msgstr "Niveau"
+
+#: waeup/kofa/students/browser.py:2958
+msgid "All Modes"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:298 waeup/kofa/students/viewlets.py:144
+msgid "Manage student section"
+msgstr "Gérer la section étudiant"
+
+#: waeup/kofa/students/browser.py:3022
+msgid "Export started for students with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3113
+msgid "Export started for course tickets with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:358 waeup/kofa/students/viewlets.py:151
+msgid "Add student"
+msgstr "Ajouter étudiant"
+
+#: waeup/kofa/students/browser.py:366
+msgid "Student record created."
+msgstr "Données étudiant créé."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:380
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:402
+#, fuzzy
+msgid "Login now"
+msgstr "Connexion"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:406
+#, fuzzy
+msgid "Login as ${a}"
+msgstr "Contacte ${a}"
+
+#: waeup/kofa/students/browser.py:412
+#, 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:430
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:432
+msgid "${a}: Base Data"
+msgstr "${a}: Données de Base"
+
+#: waeup/kofa/students/browser.py:509
+msgid "Admission Letter of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:527
+msgid "Manage base data"
+msgstr "Gestion données de base"
+
+#: waeup/kofa/students/browser.py:576
+#, fuzzy
+msgid "Trigger registration transition"
+msgstr "Transition d’Inscription:"
+
+#: waeup/kofa/students/browser.py:616
+#, fuzzy
+msgid "Student account has been activated."
+msgstr "L’étudiant a été acquitté."
+
+#: waeup/kofa/students/browser.py:635
+#, fuzzy
+msgid "Student account has been deactivated."
+msgstr "L’étudiant a été acquitté."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:670
+msgid "${a}: Clearance Data"
+msgstr "${a}: Donnée de d’acquittement"
+
+#: waeup/kofa/students/browser.py:700 waeup/kofa/students/viewlets.py:109
+#: waeup/kofa/students/viewlets.py:749
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:6
+msgid "Clearance Data"
+msgstr "Donnée d’acquittement "
+
+#: waeup/kofa/students/browser.py:706
+#, fuzzy
+msgid "Clearance Slip of"
+msgstr "Imprimé d'acquittement de"
+
+#: waeup/kofa/students/browser.py:715
+#, fuzzy
+msgid "Student Signature"
+msgstr "Section Etudiants"
+
+#: waeup/kofa/students/browser.py:716
+#, fuzzy
+msgid "Clearance Officer Signature"
+msgstr "Aquittement"
+
+#: waeup/kofa/students/browser.py:723
+#, fuzzy
+msgid "Date, Student Signature"
+msgstr "Section Etudiants"
+
+#: waeup/kofa/students/browser.py:724
+#, fuzzy
+msgid "Date, Clearance Officer Signature"
+msgstr "Aquittement"
+
+#: waeup/kofa/students/browser.py:745
+msgid "Manage clearance data"
+msgstr "Gérer les données d’acquittement"
+
+#: waeup/kofa/students/browser.py:789
+msgid "Student has been cleared."
+msgstr "L’étudiant a été acquitté."
+
+#: waeup/kofa/students/browser.py:791 waeup/kofa/students/browser.py:826
+msgid "Student is in wrong state."
+msgstr "Etudiant est dans un faux état."
+
+#: waeup/kofa/students/browser.py:803 waeup/kofa/students/viewlets.py:277
+msgid "Reject clearance"
+msgstr "Acquittement Rejeté"
+
+#: waeup/kofa/students/browser.py:815
+#, fuzzy
+msgid "Save comment and reject clearance now"
+msgstr "Sauvegarder et demander l’acquittement"
+
+#: waeup/kofa/students/browser.py:819
+msgid "Clearance has been annulled."
+msgstr "L’acquittement a été annulé."
+
+#: waeup/kofa/students/browser.py:823
+msgid "Clearance request has been rejected."
+msgstr "Demande d’acquittement rejetée."
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:856
+msgid "${a}: Personal Data"
+msgstr "${a}: Donnée à caractère personnel"
+
+#: waeup/kofa/students/browser.py:869
+#, fuzzy
+msgid "Manage personal data"
+msgstr "Gérer les données à caractère personnel"
+
+#: waeup/kofa/students/browser.py:884
+#, fuzzy
+msgid "Edit personal data"
+msgstr "Gérer les données à caractère personnel"
+
+#: waeup/kofa/students/browser.py:887
+msgid "Save/Confirm"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:914
+msgid "${a}: Study Course"
+msgstr "${a}: Cours d' étude"
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:917
+#, fuzzy
+msgid "${a}: Previous Study Course"
+msgstr "${a}: Cours d' étude"
+
+#: waeup/kofa/students/browser.py:945
+#, fuzzy
+msgid "First Study Course, "
+msgstr "Cours d' étude"
+
+#: waeup/kofa/students/browser.py:947
+#, fuzzy
+msgid "Second Study Course"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/students/browser.py:952
+#, fuzzy
+msgid "First Study Course"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/students/browser.py:963
+msgid "Manage study course"
+msgstr "Gérer Cours d' étude"
+
+#: waeup/kofa/students/browser.py:966 waeup/kofa/students/browser.py:1039
+msgid "Remove selected levels"
+msgstr "Supprimer les niveaux sélectionés"
+
+#: waeup/kofa/students/browser.py:967 waeup/kofa/students/browser.py:1019
+msgid "Add study level"
+msgstr "Ajouter niveau d’étude"
+
+#: waeup/kofa/students/browser.py:999
+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/clearanceeditpage.pt:8
+msgid "Scans"
+msgstr "Scans"
+
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:31
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:38
+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/applicants/browser_templates/applicantssearchpage.pt:10
+msgid "with registration number"
+msgstr "avec numéro d’inscription"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:11
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:11
+msgid "with matriculation number"
+msgstr "avec numéro matricule"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:12
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:12
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:78
+msgid "in session"
+msgstr "en session"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:13
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:13
+msgid "in department"
+msgstr "en département"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:14
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:14
+msgid "in study mode"
+msgstr "en mode d’étude"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:15
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:15
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:33
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:27
+msgid "Reg. Number"
+msgstr "Numéro Reg."
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:34
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:40
+msgid "Matric. Number"
+msgstr "Numéro Matricule."
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:35
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:41
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:15
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:25
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "State"
+msgstr "Etat"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:5
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:5
+msgid "for students"
+msgstr "pour les étudiants"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:8
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:8
+msgid "with id"
+msgstr "avec id"
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:9
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:9
+msgid "with name"
+msgstr "avec nom"
+
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:18
+msgid "Weight"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:20
+msgid "The activation code can be found on the payment slip."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:34
+msgid ""
+"Your data have been approved. You can continue by clicking the button below."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr "Code Activation:"
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:48
+#, fuzzy
+msgid "Current Study Mode:"
+msgstr "Mode d’Etude:"
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig.pt:1
+msgid ""
+"<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."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:12
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:2
+#: waeup/kofa/students/browser_templates/exportconfig.pt:12
+msgid "Exporter (Data Type):"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:24
+#: waeup/kofa/students/browser_templates/exportconfig.pt:24
+#, fuzzy
+msgid "Current Session:"
+msgstr "Session Actuelle"
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:36
+#: waeup/kofa/students/browser_templates/exportconfig.pt:36
+#, fuzzy
+msgid "Current Level:"
+msgstr "Niveau Actuel"
+
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:14
+#, fuzzy
+msgid "Level Session:"
+msgstr "Session"
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:50
+#, 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:93
+#: 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:94
+#: 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:90
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:46
+msgid "Payment Id"
+msgstr "Paiement Id"
+
+#: 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: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:32
+msgid "Instructions:"
+msgstr "Instructions:"
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:33
+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/studentsreportgeneratorpage.pt:10
+#, fuzzy
+msgid "Study Modes Group:"
+msgstr "Mode d’Etude:"
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.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/browser_templates/studentsreportgeneratorpage.pt:21
+#, fuzzy
+msgid "Set study modes filter."
+msgstr "en mode d’étude"
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.pt:33
+msgid "<option value=\"${DYNAMIC_CONTENT}\"> ${DYNAMIC_CONTENT} </option>"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.pt:37
+#, fuzzy
+msgid "Select academic session to report."
+msgstr "Rechercher la section universitaire"
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:6
+msgid "Study Course Data"
+msgstr "Données cours d’étude"
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:9
+msgid "Study Levels"
+msgstr "Niveaux d’Etude"
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr "Mode d’Etude:"
+
+#: 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
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:30
+msgid "Study Levels (Course Lists)"
+msgstr "Les Niveaux d’Etude (Listes de Cours)"
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:69
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:35
+msgid "Level Code"
+msgstr "Code du Niveau"
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:70
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:36
+msgid "Level Title"
+msgstr "Titre du Niveau"
+
+#: 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: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:39
+msgid "Fact."
+msgstr "Fact."
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:31
+msgid "Requ."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:43
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:21
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:34
+msgid "CO"
+msgstr "CO"
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:44
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:35
+msgid "Auto"
+msgstr "Auto"
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:6
+msgid "Study Level Data"
+msgstr "Donnée Niveau Etude"
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:87
+#: 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/studylevelmanagepage.pt:93
+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:28
+msgid "Dep."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:29
+msgid "Fac."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:58
+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/export.py:220
+#, fuzzy
+msgid "Student Study Courses"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/students/export.py:250
+#, fuzzy
+msgid "Student Study Levels"
+msgstr "Niveaux d’Etude"
+
+#: waeup/kofa/students/export.py:275
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:9
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:30
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:20
+msgid "Course Tickets"
+msgstr "Tickets de Cours"
+
+#: waeup/kofa/students/export.py:309
+#, fuzzy
+msgid "Student Payments"
+msgstr "Etudiants admis"
+
+#: waeup/kofa/students/export.py:346
+#, fuzzy
+msgid "Payment Data for Bursary"
+msgstr "Données de paiement"
+
+#: waeup/kofa/students/export.py:377
+#, fuzzy
+msgid "Bed Tickets"
+msgstr "Ticket de lit ${a}"
+
+#: waeup/kofa/students/export.py:413
+#, fuzzy
+msgid "Student Payments Overview"
+msgstr "Etudiants admis"
+
+#: waeup/kofa/students/export.py:442
+#, fuzzy
+msgid "Student Study Levels Overview"
+msgstr "Niveaux d’Etude"
+
+#: waeup/kofa/students/export.py:471
+msgid "Combo Card Data"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:191
+#, fuzzy
+msgid "Reasons for Deactivation"
+msgstr "Description de Réponse"
+
+#: waeup/kofa/students/interfaces.py:193
+msgid ""
+"This message will be shown if and only if deactivated students try to login."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:232 waeup/kofa/students/interfaces.py:365
+msgid "Matriculation Number"
+msgstr "Numéro d’Immatriculation"
+
+#: waeup/kofa/students/interfaces.py:239
+msgid "PWD Activation Code"
+msgstr "PWD Code Activation"
+
+#: waeup/kofa/students/interfaces.py:245
+msgid "Email"
+msgstr "Email"
+
+#: waeup/kofa/students/interfaces.py:280
+msgid "Officer's Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:285
+msgid "Clearance form locked"
+msgstr "Formulaire d’acquittement vérouillé"
+
+#: waeup/kofa/students/interfaces.py:291
+msgid "CLR Activation Code"
+msgstr "CLR Code Activation"
+
+#: waeup/kofa/students/interfaces.py:304
+msgid "Nationality"
+msgstr "Nationalité"
+
+#: waeup/kofa/students/interfaces.py:313
+msgid "Employer"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:323
+#, fuzzy
+msgid "Updated"
+msgstr "Mettre à jour tous les lits"
+
+#: waeup/kofa/students/interfaces.py:329 waeup/kofa/students/interfaces.py:347
+msgid "Permanent Address"
+msgstr "Adresse Permanente"
+
+#: waeup/kofa/students/interfaces.py:376
+#, fuzzy
+msgid "Registr. or Matric. Number"
+msgstr "Numéro d’Inscription"
+
+#: waeup/kofa/students/interfaces.py:396 waeup/kofa/students/interfaces.py:450
+#: waeup/kofa/browser/templates/showstudentspage.pt:7
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:59
+msgid "Certificate"
+msgstr "Certificat"
+
+#: waeup/kofa/students/interfaces.py:402 waeup/kofa/students/interfaces.py:482
+msgid "Entry Mode"
+msgstr "Mode d’Entrée"
+
+#: waeup/kofa/students/interfaces.py:409 waeup/kofa/students/interfaces.py:463
+#: waeup/kofa/students/interfaces.py:489
+msgid "Entry Session"
+msgstr "Session d’Entrée"
+
+#: waeup/kofa/students/interfaces.py:431 waeup/kofa/students/interfaces.py:502
+msgid "Current Verdict"
+msgstr "Verdict Actuel"
+
+#: waeup/kofa/students/interfaces.py:438
+msgid "Previous Verdict"
+msgstr "Verdict Précédent"
+
+#: waeup/kofa/students/interfaces.py:520
+msgid "Bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:525 waeup/kofa/students/interfaces.py:552
+#, fuzzy
+msgid "Validated by"
+msgstr "invalidé"
+
+#: waeup/kofa/students/interfaces.py:545
+msgid "Verdict"
+msgstr "Verdict"
+
+#: waeup/kofa/students/interfaces.py:558
+#, fuzzy
+msgid "Validation Date"
+msgstr "Date de Création"
+
+#: waeup/kofa/students/interfaces.py:564
+#, fuzzy
+msgid "Total Credits"
+msgstr "Crédits Total:"
+
+#: waeup/kofa/students/interfaces.py:602
+#, fuzzy
+msgid "Faculty Code"
+msgstr "Faculté"
+
+#: waeup/kofa/students/interfaces.py:607
+#, fuzzy
+msgid "Department Code"
+msgstr "Département"
+
+#: waeup/kofa/students/interfaces.py:612
+#: waeup/kofa/university/interfaces.py:164
+msgid "Semester/Term"
+msgstr "Trimestre/Semestre"
+
+#: waeup/kofa/students/interfaces.py:618
+#: waeup/kofa/university/interfaces.py:158
+msgid "Passmark"
+msgstr "Moyenne"
+
+#: waeup/kofa/students/interfaces.py:628
+#: waeup/kofa/browser/templates/certificatepage.pt:33
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:39
+msgid "Required"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:640
+msgid "Carry-over Course"
+msgstr "Cours Echoué"
+
+#: waeup/kofa/students/interfaces.py:646
+msgid "Automatical Creation"
+msgstr "Création Automatique"
+
+#: waeup/kofa/students/interfaces.py:657
+#: waeup/kofa/university/interfaces.py:334
+#: waeup/kofa/university/interfaces.py:369
+#: waeup/kofa/students/browser_templates/transcript.pt:37
+msgid "Course"
+msgstr "Cours"
+
+#: waeup/kofa/students/interfaces.py:673
+#, fuzzy
+msgid "Level Session"
+msgstr "Session"
+
+#: waeup/kofa/students/interfaces.py:702
+#, fuzzy
+msgid "Allocated Bed"
+msgstr "Allouer étudiant"
+
+#: waeup/kofa/students/interfaces.py:708
+#, fuzzy
+msgid "Requested Bed Type"
+msgstr "Type de lit"
+
+#: waeup/kofa/students/interfaces.py:721
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr "Date de Réservation"
+
+#: waeup/kofa/students/interfaces.py:727
+msgid "Booking Activation Code"
+msgstr "Code Activation de Réservation"
+
+#: waeup/kofa/students/interfaces.py:748
+#, fuzzy
+msgid "Current Session Payment"
+msgstr "Session Actuelle"
+
+#: waeup/kofa/students/interfaces.py:754 waeup/kofa/students/interfaces.py:795
+#: waeup/kofa/students/interfaces.py:819
+#, fuzzy
+msgid "Payment Level"
+msgstr "Item de Paiement"
+
+#: waeup/kofa/students/interfaces.py:825
+msgid "Balance Amount"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:829
+msgid "Balance in Naira "
+msgstr ""
+
+#: waeup/kofa/students/reports.py:176
+#, fuzzy
+msgid "Create students report"
+msgstr "Créer les données d’étudiant"
+
+#: waeup/kofa/students/reports.py:196
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:122
+#, fuzzy
+msgid "Name:"
+msgstr "Nom"
+
+#: waeup/kofa/students/utils.py:141
+#, fuzzy
+msgid "Study Course:"
+msgstr "Cours d' étude"
+
+#: waeup/kofa/students/utils.py:148 waeup/kofa/students/utils.py:734
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr "Département:"
+
+#: waeup/kofa/students/utils.py:157 waeup/kofa/students/utils.py:727
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr "Faculté"
+
+#: waeup/kofa/students/utils.py:166 waeup/kofa/students/utils.py:741
+#, fuzzy
+msgid "Entry Session:"
+msgstr "Session d’Entrée"
+
+#: waeup/kofa/students/utils.py:174
+#, fuzzy
+msgid "Date of Birth:"
+msgstr "Date de naissance"
+
+#: waeup/kofa/students/utils.py:261
+msgid "Scanned Documents"
+msgstr "Documents Scannés"
+
+#: waeup/kofa/students/utils.py:338
+msgid "The previous session must not fall below your entry session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:343 waeup/kofa/students/utils.py:346
+msgid "This is not a previous session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:363 waeup/kofa/students/utils.py:402
+#, fuzzy
+msgid "Study course data are incomplete."
+msgstr "Vos données sont incompletes."
+
+#: waeup/kofa/students/utils.py:417
+#, fuzzy
+msgid "no bed allocated"
+msgstr "Télécharger imprimé du lit alloué"
+
+#: waeup/kofa/students/utils.py:419
+#, fuzzy
+msgid "Amount could not be determined."
+msgstr "Données étudiant créé."
+
+#: waeup/kofa/students/utils.py:452
+#, fuzzy
+msgid "Amount must be greater than 0."
+msgstr "Données étudiant créé."
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:516
+msgid ""
+"This is to inform you that you have been provisionally admitted into ${a} as "
+"follows:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:556
+#, fuzzy
+msgid "Your Kofa student record was created on ${a}."
+msgstr "Données étudiant créé."
+
+#: waeup/kofa/students/utils.py:608 waeup/kofa/students/viewlets.py:76
+#: waeup/kofa/students/viewlets.py:104
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:7
+msgid "Base Data"
+msgstr "Données de Base"
+
+#: waeup/kofa/students/utils.py:628
+#, fuzzy
+msgid "Workflow History"
+msgstr "État d’avancement"
+
+#: waeup/kofa/students/utils.py:721
+#, fuzzy
+msgid "Course of Study:"
+msgstr "Admis au cours:"
+
+#: waeup/kofa/students/utils.py:748
+#, fuzzy
+msgid "Entry Mode:"
+msgstr "Mode d’Entrée"
+
+#: waeup/kofa/students/utils.py:755
+msgid "Cumulative GPA:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:786
+msgid "Error in image file."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:814
+msgid "(not yet)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:114 waeup/kofa/students/viewlets.py:750
+msgid "Personal Data"
+msgstr "Données Personnels"
+
+#: waeup/kofa/students/viewlets.py:136 waeup/kofa/students/viewlets.py:755
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:27
+msgid "History"
+msgstr "Histoire"
+
+#: waeup/kofa/students/viewlets.py:177
+#, fuzzy
+msgid "Trigger transition"
+msgstr "Pas de transition"
+
+#: waeup/kofa/students/viewlets.py:186
+#, fuzzy
+msgid "Login as student"
+msgstr "pour les étudiants"
+
+#: waeup/kofa/students/viewlets.py:195
+#, fuzzy
+msgid "Download admission letter"
+msgstr "Télécharger la demande en veille"
+
+#: waeup/kofa/students/viewlets.py:212
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:224 waeup/kofa/students/viewlets.py:244
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:232
+msgid "Activate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:260 waeup/kofa/students/workflow.py:76
+msgid "Clear student"
+msgstr "Etudiant Acquitté"
+
+#: waeup/kofa/students/viewlets.py:295
+msgid "Download clearance slip"
+msgstr "Télécharger imprimé d' acquittement"
+
+#: waeup/kofa/students/viewlets.py:304 waeup/kofa/students/viewlets.py:313
+msgid "View"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:351
+#, fuzzy
+msgid "Transcript"
+msgstr "Transfère"
+
+#: waeup/kofa/students/viewlets.py:382
+msgid "Reactivate this study course (revert previous transfer)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:411 waeup/kofa/students/workflow.py:160
+msgid "Validate courses"
+msgstr "Valider Cours"
+
+#: waeup/kofa/students/viewlets.py:429
+msgid "Reject courses"
+msgstr "Rejeter Cours"
+
+#: waeup/kofa/students/viewlets.py:448
+msgid "Download course registration slip"
+msgstr "Télécharger imprimé des cours inscrit"
+
+#: waeup/kofa/students/viewlets.py:510
+msgid "Book accommodation"
+msgstr "Réserver chambre"
+
+#: waeup/kofa/students/viewlets.py:519
+msgid "Download bed allocation slip"
+msgstr "Télécharger imprimé du lit alloué"
+
+#: waeup/kofa/students/viewlets.py:528
+msgid "Relocate student"
+msgstr "Relocaliser étudiant"
+
+#: waeup/kofa/students/viewlets.py:554
+msgid "Change portrait"
+msgstr "Changer portrait"
+
+#: waeup/kofa/students/viewlets.py:598
+#, fuzzy
+msgid "Start new session"
+msgstr "Session"
+
+#: waeup/kofa/students/viewlets.py:612
+msgid "Add course list"
+msgstr "Ajouter liste de cours"
+
+#: waeup/kofa/students/viewlets.py:631
+#, fuzzy
+msgid "Edit course list"
+msgstr "Ajouter liste de cours"
+
+#: waeup/kofa/students/viewlets.py:649
+#, fuzzy
+msgid "Add current session payment ticket"
+msgstr "Ajout de ticket de paiement en ligne"
+
+#: waeup/kofa/students/viewlets.py:657
+#, fuzzy
+msgid "Add previous session payment ticket"
+msgstr "Ajouter paiement en ligne"
+
+#: waeup/kofa/students/viewlets.py:672
+#, fuzzy
+msgid "Add balance payment ticket"
+msgstr "Ajout de ticket de paiement en ligne"
+
+#: waeup/kofa/students/viewlets.py:754
+msgid "Accommodation Data"
+msgstr "Données d’hébergement"
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:766
+msgid "${a} deleted."
+msgstr "${a} supprimé."
+
+#: waeup/kofa/students/viewlets.py:780
+msgid "Uploaded file is too big."
+msgstr "Le fichier envoyé est trop grand."
+
+#: waeup/kofa/students/viewlets.py:786
+#, fuzzy
+msgid "Could not determine file type."
+msgstr "Ne pourrait être supprimer:"
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:791
+msgid "${a} file extension expected."
+msgstr "${a} l’extension de fichier attendue."
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:797
+#, fuzzy
+msgid "Only the following extensions are allowed: ${a}"
+msgstr "Seulement les extensions suivantes sont autorisées: ${a}"
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:806
+msgid "File ${a} uploaded."
+msgstr "Fichier ${a} chargé."
+
+#: waeup/kofa/students/viewlets.py:824
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:10
+#: waeup/kofa/browser/templates/datacenterpage.pt:17
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:11
+msgid "File"
+msgstr "Fichier"
+
+#: waeup/kofa/students/viewlets.py:825
+msgid "Scan"
+msgstr "Scan"
+
+#: waeup/kofa/students/viewlets.py:848
+msgid "Upload new file"
+msgstr "Envoyer un nouveau fichier"
+
+#: waeup/kofa/students/viewlets.py:849
+msgid "Delete attachment"
+msgstr "Supprimer lien"
+
+#: waeup/kofa/students/viewlets.py:886
+msgid "No local file selected."
+msgstr "Aucun fichier local sélectioné"
+
+#: waeup/kofa/students/viewlets.py:90
+msgid "Application Slip"
+msgstr "Imprimé de demande"
+
+#: waeup/kofa/students/viewlets.py:900
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:9
+msgid "Passport Picture"
+msgstr "Photo d’Identité"
+
+#: waeup/kofa/students/viewlets.py:911
+msgid "Passport Picture (jpg only)"
+msgstr "Photo d’Identité (jpg seulement)"
+
+#: waeup/kofa/students/viewlets.py:926 waeup/kofa/students/viewlets.py:937
+msgid "Birth Certificate"
+msgstr "Extrait d’Acte de Naissance"
+
+#: waeup/kofa/students/viewlets.py:927 waeup/kofa/students/viewlets.py:938
+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: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:10
+#: waeup/kofa/browser/templates/departmentpage.pt:3
+#: waeup/kofa/browser/templates/certificatepage.pt:22
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:28
+msgid "Courses"
+msgstr "Cours"
+
+#: waeup/kofa/university/export.py:165
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:13
+#: 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:146
+msgid "Title of course"
+msgstr "Titre du cours"
+
+#: waeup/kofa/university/interfaces.py:171
+#, fuzzy
+msgid "Former Course"
+msgstr "Cours Echoué"
+
+#: waeup/kofa/university/interfaces.py:172
+msgid ""
+"If this attribute is being set all certificate courses referring to this "
+"course will be automatically deleted."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:189
+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."
+
+#: waeup/kofa/university/interfaces.py:209
+#, fuzzy
+msgid "Unnamed Certificate"
+msgstr "Gérer certificact"
+
+#: waeup/kofa/university/interfaces.py:214
+msgid "Study Mode"
+msgstr "Mode d’Etude"
+
+#: waeup/kofa/university/interfaces.py:221
+msgid "Start Level"
+msgstr "Niveau de démarrage"
+
+#: waeup/kofa/university/interfaces.py:228
+msgid "End Level"
+msgstr "Niveau final"
+
+#: waeup/kofa/university/interfaces.py:235
+msgid "Application Category"
+msgstr "Catégorie de demande"
+
+#: waeup/kofa/university/interfaces.py:242
+#, fuzzy
+msgid "Initial School Fee"
+msgstr "Frais Scolaires"
+
+#: waeup/kofa/university/interfaces.py:247
+#, fuzzy
+msgid "Returning School Fee"
+msgstr "Frais Scolaires"
+
+#: waeup/kofa/university/interfaces.py:252
+#, fuzzy
+msgid "Foreigner Initial School Fee"
+msgstr "Frais Scolaires"
+
+#: waeup/kofa/university/interfaces.py:257
+#, fuzzy
+msgid "Foreigner Returning School Fee"
+msgstr "Frais Scolaires"
+
+#: waeup/kofa/university/interfaces.py:262
+msgid "Installment Ratio"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:269
+msgid "Custom Textline 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:274
+msgid "Custom Textline 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:279
+msgid "Custom Float 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:284
+msgid "Custom Float 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:296
+#: waeup/kofa/university/interfaces.py:298
+msgid "Start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:300
+#: waeup/kofa/university/interfaces.py:303
+msgid "Study mode, start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:313
+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."
+
+#: waeup/kofa/university/interfaces.py:347
+#, fuzzy
+msgid "Registration required"
+msgstr "Numéro d’Inscription"
+
+#: waeup/kofa/university/interfaces.py:45
+msgid "Name of faculty"
+msgstr "Nom de la Faculté"
+
+#: waeup/kofa/university/interfaces.py:46
+#: waeup/kofa/university/interfaces.py:94
+#: waeup/kofa/university/interfaces.py:147
+msgid "Unnamed"
+msgstr "Anonyme"
+
+#: waeup/kofa/university/interfaces.py:51
+#: waeup/kofa/university/interfaces.py:99
+msgid "Name prefix"
+msgstr "Préfixe du nom"
+
+#: waeup/kofa/university/interfaces.py:67
+#: waeup/kofa/university/interfaces.py:118
+msgid "This code will become part of the URL."
+msgstr "Ce code fera partie de l’URL."
+
+#: waeup/kofa/university/interfaces.py:93
+msgid "Name of department"
+msgstr "Nom du département"
+
+#: waeup/kofa/university/vocabularies.py:29
+msgid "Pre-Studies"
+msgstr "Pré-études"
+
+#: waeup/kofa/university/vocabularies.py:30
+msgid "100 (Year 1)"
+msgstr "100 (Année 1)"
+
+#: waeup/kofa/university/vocabularies.py:31
+msgid "200 (Year 2)"
+msgstr "200 (Année 2)"
+
+#: waeup/kofa/university/vocabularies.py:32
+msgid "300 (Year 3)"
+msgstr "300 (Année 3)"
+
+#: waeup/kofa/university/vocabularies.py:33
+msgid "400 (Year 4)"
+msgstr "400 (Année 4)"
+
+#: waeup/kofa/university/vocabularies.py:34
+msgid "500 (Year 5)"
+msgstr "500 (Année 5)"
+
+#: waeup/kofa/university/vocabularies.py:35
+msgid "600 (Year 6)"
+msgstr "600 (Année 6)"
+
+#: waeup/kofa/university/vocabularies.py:36
+msgid "700 (Year 7)"
+msgstr "700 (Année 7)"
+
+#: waeup/kofa/university/vocabularies.py:37
+msgid "800 (Year 8)"
+msgstr "800 (Année 8)"
+
+#: waeup/kofa/university/vocabularies.py:38
+#, fuzzy
+msgid "900 (Year 9)"
+msgstr "100 (Année 1)"
+
+#: waeup/kofa/university/vocabularies.py:39
+#, 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:186
+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:239
+#, 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:18
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/kofa/widgets/sequencewidget.pt:21
+#, fuzzy
+msgid "remove-selected-items"
+msgstr "Supprimer les tickets sélectionnés"
+
+#~ 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/tags/0.2/src/waeup/kofa/locales/ha/LC_MESSAGES/waeup.kofa.po
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/ha/LC_MESSAGES/waeup.kofa.po	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/ha/LC_MESSAGES/waeup.kofa.po	(revision 10886)
@@ -0,0 +1,6189 @@
+# #############################################################################
+#
+# 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: Thu Jun 27 09:36:46 2013\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:102 waeup/kofa/accesscodes/browser.py:115
+#: waeup/kofa/applicants/browser.py:262 waeup/kofa/applicants/browser.py:351
+#: waeup/kofa/applicants/browser.py:352 waeup/kofa/applicants/browser.py:472
+#: waeup/kofa/browser/pages.py:636 waeup/kofa/browser/pages.py:846
+#: waeup/kofa/browser/pages.py:872 waeup/kofa/browser/pages.py:939
+#: waeup/kofa/browser/pages.py:1171 waeup/kofa/browser/pages.py:1201
+#: waeup/kofa/browser/pages.py:1304 waeup/kofa/browser/pages.py:1535
+#: waeup/kofa/browser/pages.py:1701 waeup/kofa/browser/pages.py:1725
+#: waeup/kofa/browser/pages.py:1756 waeup/kofa/browser/pages.py:1784
+#: waeup/kofa/browser/pages.py:1785 waeup/kofa/browser/pages.py:1828
+#: waeup/kofa/browser/pages.py:1873 waeup/kofa/browser/pages.py:1918
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1921
+#: waeup/kofa/browser/pages.py:1996 waeup/kofa/browser/pages.py:2045
+#: waeup/kofa/browser/pages.py:2086 waeup/kofa/browser/pages.py:2122
+#: waeup/kofa/browser/pages.py:2161 waeup/kofa/browser/pages.py:2163
+#: waeup/kofa/browser/pages.py:2196 waeup/kofa/browser/pages.py:2247
+#: waeup/kofa/browser/pages.py:2304 waeup/kofa/students/browser.py:965
+#: waeup/kofa/students/browser.py:966 waeup/kofa/students/browser.py:1341
+#: waeup/kofa/students/browser.py:1343 waeup/kofa/students/browser.py:1500
+#: waeup/kofa/students/browser.py:1659 waeup/kofa/students/browser.py:1697
+#: waeup/kofa/students/browser.py:1730
+msgid "Cancel"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:104
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:114
+msgid "Reimport"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:117
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:122
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:128
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:138
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:141
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:154
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:155 waeup/kofa/applicants/browser.py:85
+#: waeup/kofa/applicants/browser.py:113 waeup/kofa/browser/pages.py:725
+#: waeup/kofa/browser/pages.py:1495 waeup/kofa/browser/viewlets.py:200
+#: waeup/kofa/students/browser.py:258 waeup/kofa/students/browser.py:299
+msgid "Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:156
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:157
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:182 waeup/kofa/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:184
+msgid "${a} disabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:188
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:192
+msgid "re-enabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:194
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:198
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:237
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:246
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:254
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:263
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:44
+msgid "Archive"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:45
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:47 waeup/kofa/accesscodes/browser.py:208
+#: waeup/kofa/accesscodes/browser.py:228
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:54
+msgid "No batch selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:62
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:70
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:79
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:85
+msgid "Create batch"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:93
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:95
+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:23
+msgid "Serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:24
+msgid "AC"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:26
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:48
+msgid "Owner"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:5
+msgid "for access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:7
+msgid "with PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:8
+msgid "with serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:9
+msgid "with message term"
+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:119
+msgid "Applicant has not yet been admitted."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:125
+msgid "Registration Number exists."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:128
+msgid "No course admitted provided."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/applicant.py:160
+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:1002
+msgid "1st and 2nd choice must be different."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1020
+msgid "The form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1026
+msgid "Form has been submitted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:1069
+msgid "Apply for ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1074
+msgid "Outside application period."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1089 waeup/kofa/browser/pages.py:2353
+#: waeup/kofa/students/browser.py:2700
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1112 waeup/kofa/students/browser.py:2717
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1118 waeup/kofa/applicants/browser.py:1130
+msgid "No application record found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:112 waeup/kofa/applicants/viewlets.py:90
+msgid "Search applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1122 waeup/kofa/students/browser.py:2726
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1141
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1158
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:128 waeup/kofa/students/browser.py:276
+#: waeup/kofa/students/browser.py:319
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:133
+msgid "No applicant found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:141 waeup/kofa/applicants/viewlets.py:99
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:144 waeup/kofa/applicants/browser.py:219
+#: waeup/kofa/applicants/browser.py:351 waeup/kofa/applicants/browser.py:404
+#: waeup/kofa/applicants/browser.py:795 waeup/kofa/applicants/browser.py:841
+#: waeup/kofa/applicants/browser.py:949 waeup/kofa/applicants/browser.py:952
+#: waeup/kofa/applicants/browser.py:996 waeup/kofa/browser/pages.py:607
+#: waeup/kofa/browser/pages.py:764 waeup/kofa/browser/pages.py:787
+#: waeup/kofa/browser/pages.py:866 waeup/kofa/browser/pages.py:1533
+#: waeup/kofa/browser/pages.py:1784 waeup/kofa/browser/pages.py:1824
+#: waeup/kofa/browser/pages.py:1918 waeup/kofa/browser/pages.py:1967
+#: waeup/kofa/browser/pages.py:2118 waeup/kofa/browser/pages.py:2161
+#: waeup/kofa/browser/pages.py:2182 waeup/kofa/hostels/browser.py:131
+#: waeup/kofa/hostels/browser.py:176 waeup/kofa/hostels/browser.py:241
+#: waeup/kofa/hostels/browser.py:265 waeup/kofa/hostels/browser.py:360
+#: waeup/kofa/students/browser.py:542 waeup/kofa/students/browser.py:595
+#: waeup/kofa/students/browser.py:770 waeup/kofa/students/browser.py:872
+#: waeup/kofa/students/browser.py:965 waeup/kofa/students/browser.py:993
+#: waeup/kofa/students/browser.py:1341 waeup/kofa/students/browser.py:1386
+#: waeup/kofa/students/browser.py:2173 waeup/kofa/students/browser.py:2188
+#: waeup/kofa/students/browser.py:2311
+msgid "Save"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:145 waeup/kofa/applicants/browser.py:175
+#: waeup/kofa/applicants/browser.py:352 waeup/kofa/applicants/browser.py:423
+#: waeup/kofa/browser/pages.py:767 waeup/kofa/browser/pages.py:809
+#: waeup/kofa/browser/pages.py:893 waeup/kofa/browser/pages.py:1701
+#: waeup/kofa/browser/pages.py:1712 waeup/kofa/browser/pages.py:1785
+#: waeup/kofa/browser/pages.py:1816 waeup/kofa/hostels/browser.py:134
+#: waeup/kofa/hostels/browser.py:149 waeup/kofa/students/browser.py:300
+#: waeup/kofa/students/browser.py:1840 waeup/kofa/students/browser.py:1853
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:145 waeup/kofa/applicants/browser.py:200
+#: waeup/kofa/applicants/browser.py:231 waeup/kofa/applicants/browser.py:241
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:146 waeup/kofa/applicants/browser.py:209
+#: waeup/kofa/applicants/browser.py:354 waeup/kofa/applicants/browser.py:481
+#: waeup/kofa/browser/pages.py:1786 waeup/kofa/browser/pages.py:1842
+#: waeup/kofa/browser/pages.py:1922 waeup/kofa/browser/pages.py:2005
+#: waeup/kofa/browser/pages.py:2164 waeup/kofa/browser/pages.py:2215
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:147 waeup/kofa/applicants/browser.py:205
+#: waeup/kofa/applicants/browser.py:355 waeup/kofa/applicants/browser.py:477
+#: waeup/kofa/browser/pages.py:1787 waeup/kofa/browser/pages.py:1838
+#: waeup/kofa/browser/pages.py:1923 waeup/kofa/browser/pages.py:2001
+#: waeup/kofa/browser/pages.py:2165 waeup/kofa/browser/pages.py:2211
+msgid "Add local role"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:148
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:181
+msgid "No container selected!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:192 waeup/kofa/applicants/browser.py:440
+#: waeup/kofa/applicants/browser.py:896
+msgid "Could not delete:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:195 waeup/kofa/applicants/browser.py:443
+#: waeup/kofa/applicants/browser.py:899 waeup/kofa/browser/pages.py:153
+#: waeup/kofa/students/browser.py:346 waeup/kofa/students/browser.py:1055
+#: waeup/kofa/students/browser.py:1407 waeup/kofa/students/browser.py:1602
+#: waeup/kofa/students/browser.py:1873 waeup/kofa/students/browser.py:2549
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:223 waeup/kofa/applicants/browser.py:418
+#: waeup/kofa/applicants/browser.py:869 waeup/kofa/applicants/browser.py:1005
+#: waeup/kofa/browser/pages.py:195 waeup/kofa/browser/pages.py:2295
+#: waeup/kofa/hostels/browser.py:55 waeup/kofa/students/browser.py:100
+#: waeup/kofa/students/browser.py:564
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:250
+msgid ""
+"An applicants container for the same application type and entrance year "
+"exists already in the database."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:258
+msgid "Added:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:270 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:353 waeup/kofa/applicants/browser.py:448
+msgid "Create students from selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:361
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:429 waeup/kofa/applicants/browser.py:454
+msgid "No applicant selected!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:465 waeup/kofa/applicants/browser.py:614
+msgid "${a} students successfully created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:468 waeup/kofa/applicants/browser.py:617
+msgid "No student could be created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:495 waeup/kofa/applicants/viewlets.py:115
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:498
+msgid "Create application record"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:503
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:517
+msgid "Applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:541 waeup/kofa/students/browser.py:438
+msgid "set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:542 waeup/kofa/students/browser.py:439
+msgid "unset"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:547
+msgid "${a} <br /> Application Record ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:637 waeup/kofa/students/utils.py:426
+#: waeup/kofa/students/utils.py:459
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:649 waeup/kofa/students/browser.py:1655
+#: waeup/kofa/students/browser.py:1693 waeup/kofa/students/browser.py:1726
+msgid "Payment ticket created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:672 waeup/kofa/students/browser.py:1749
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:68
+msgid ""
+"'You can not edit your application records after final submission. You "
+"really want to submit?'"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:717 waeup/kofa/students/browser.py:1806
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:723 waeup/kofa/students/browser.py:1812
+msgid "Online Payment Slip"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:749
+msgid "Please pay and submit before trying to download the application slip."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:770
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:775
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:795 waeup/kofa/applicants/browser.py:952
+#: waeup/kofa/applicants/browser.py:1008
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:796 waeup/kofa/applicants/browser.py:908
+#: waeup/kofa/applicants/browser.py:950 waeup/kofa/applicants/browser.py:985
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:796 waeup/kofa/applicants/browser.py:913
+#: waeup/kofa/applicants/browser.py:950 waeup/kofa/applicants/browser.py:953
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/students/browser.py:1343
+#: waeup/kofa/students/browser.py:1391 waeup/kofa/students/browser.py:1583
+#: waeup/kofa/students/browser.py:2557
+msgid "Remove selected tickets"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:826
+msgid "${a} <br /> Application Form ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:838 waeup/kofa/students/browser.py:592
+msgid "No transition"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:84
+msgid "Application Section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:883 waeup/kofa/students/browser.py:1589
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:961 waeup/kofa/students/browser.py:108
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:977
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:979
+msgid "Passport picture confirmation box not ticked."
+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:28
+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:25
+msgid "Passport Photo:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:50
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:66
+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:82
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:40
+msgid "Application Fee Payment Tickets"
+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/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:46
+msgid "Application Number"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:50
+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:92
+msgid "note_acmp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:93
+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:38
+msgid "note_armp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:56
+msgid "Records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:6
+msgid "Introduction"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:12
+msgid "Application Period"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:50
+msgid "Year"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:11
+msgid "with email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:5
+msgid "for applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/export.py:39
+msgid "Basic Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:143
+msgid "Create Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:144
+msgid "Update Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:174
+#: waeup/kofa/applicants/interfaces.py:238
+msgid "Human readable description in HTML format"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:192 waeup/kofa/students/browser.py:1130
+#: waeup/kofa/students/browser.py:1290 waeup/kofa/university/interfaces.py:38
+#: waeup/kofa/university/interfaces.py:66
+#: waeup/kofa/university/interfaces.py:86
+#: waeup/kofa/university/interfaces.py:117
+#: waeup/kofa/university/interfaces.py:139
+#: waeup/kofa/university/interfaces.py:188
+#: waeup/kofa/university/interfaces.py:201
+#: waeup/kofa/university/interfaces.py:311
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:39
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:68
+#: waeup/kofa/browser/templates/facultypage.pt:8
+#: waeup/kofa/browser/templates/departmentpage.pt:15
+#: waeup/kofa/browser/templates/departmentpage.pt:38
+#: waeup/kofa/browser/templates/searchpage.pt:13
+#: waeup/kofa/browser/templates/facultymanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:15
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:26
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:52
+msgid "Code"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:198 waeup/kofa/students/browser.py:1131
+#: waeup/kofa/students/browser.py:1291 waeup/kofa/students/interfaces.py:597
+#: waeup/kofa/university/interfaces.py:208
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:70
+#: waeup/kofa/browser/templates/facultypage.pt:9
+#: waeup/kofa/browser/templates/departmentpage.pt:16
+#: waeup/kofa/browser/templates/departmentpage.pt:39
+#: waeup/kofa/browser/templates/searchpage.pt:14
+#: waeup/kofa/browser/templates/certificatepage.pt:32
+#: waeup/kofa/browser/templates/facultymanagepage.pt:43
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:38
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:37
+#: waeup/kofa/students/browser_templates/transcript.pt:38
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:16
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:27
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:54
+msgid "Title"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:204
+#: waeup/kofa/applicants/interfaces.py:318
+msgid "Application Target"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:211
+#: waeup/kofa/applicants/interfaces.py:325
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:218
+msgid "Application Mode"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:232
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:251
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:253
+#: waeup/kofa/applicants/interfaces.py:259 waeup/kofa/hostels/interfaces.py:39
+#: waeup/kofa/hostels/interfaces.py:45
+msgid "Example:"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:257
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:263
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:269 waeup/kofa/interfaces.py:652
+msgid "Application Fee"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:275
+msgid "Hide container"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:353
+#: waeup/kofa/students/interfaces.py:185
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:359
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:26
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:364
+#: waeup/kofa/students/interfaces.py:225 waeup/kofa/students/interfaces.py:356
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:375
+#: waeup/kofa/applicants/interfaces.py:526
+#: waeup/kofa/students/interfaces.py:204 waeup/kofa/students/interfaces.py:381
+msgid "First Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:379
+#: waeup/kofa/students/interfaces.py:209
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:383
+#: waeup/kofa/students/interfaces.py:214
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:387
+#: waeup/kofa/students/interfaces.py:297
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:393
+#: waeup/kofa/students/interfaces.py:219
+msgid "Sex"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:398
+#: waeup/kofa/applicants/interfaces.py:474
+#: waeup/kofa/applicants/interfaces.py:531
+#: waeup/kofa/browser/interfaces.py:221 waeup/kofa/interfaces.py:435
+#: waeup/kofa/interfaces.py:511 waeup/kofa/students/interfaces.py:386
+msgid "Email Address"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:403 waeup/kofa/interfaces.py:440
+#: waeup/kofa/interfaces.py:518 waeup/kofa/students/interfaces.py:250
+msgid "Phone"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:408
+#: waeup/kofa/applicants/interfaces.py:479
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:413
+#: waeup/kofa/applicants/interfaces.py:484
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:425
+#: waeup/kofa/applicants/interfaces.py:495
+msgid "Notice"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:429
+#: waeup/kofa/students/interfaces.py:199
+#: waeup/kofa/browser/templates/showstudentspage.pt:4
+#: waeup/kofa/students/browser_templates/containerpage.pt:32
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:38
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:56
+msgid "Student Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:434
+#: waeup/kofa/applicants/interfaces.py:489
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:439
+msgid "Form locked"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:53 waeup/kofa/applicants/workflow.py:74
+#: waeup/kofa/students/payments.py:115
+msgid "Payment approved"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:66 waeup/kofa/students/payments.py:104
+msgid "Successful payment"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:73 waeup/kofa/students/payments.py:122
+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:40
+msgid "Desired Study Courses"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:41
+msgid "Process Data"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:56 waeup/kofa/students/browser.py:139
+#: waeup/kofa/students/utils.py:356 waeup/kofa/students/utils.py:383
+#: waeup/kofa/students/utils.py:394 waeup/kofa/students/utils.py:450
+msgid "Session configuration object is not available."
+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:146 waeup/kofa/applicants/viewlets.py:155
+msgid "Create students"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:163
+msgid "View application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:171
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:179
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:197
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:213 waeup/kofa/applicants/workflow.py:108
+#: waeup/kofa/students/browser.py:361
+msgid "Create student record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:230 waeup/kofa/students/viewlets.py:480
+msgid "Download payment slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:245 waeup/kofa/applicants/workflow.py:73
+#: waeup/kofa/students/viewlets.py:495 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:728
+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:91
+msgid "admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:44
+msgid "not admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:45 waeup/kofa/interfaces.py:90
+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:93
+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:652
+msgid "Academics"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:127 waeup/kofa/browser/pages.py:460
+msgid "Administration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:134 waeup/kofa/browser/viewlets.py:335
+#: waeup/kofa/browser/viewlets.py:703
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:152 waeup/kofa/browser/pages.py:507
+#: waeup/kofa/browser/viewlets.py:344 waeup/kofa/browser/viewlets.py:713
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:159 waeup/kofa/browser/pages.py:885
+#: waeup/kofa/browser/pages.py:1493 waeup/kofa/browser/viewlets.py:353
+#: waeup/kofa/browser/viewlets.py:722
+msgid "Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:166 waeup/kofa/browser/reports.py:37
+#: waeup/kofa/browser/viewlets.py:362 waeup/kofa/browser/viewlets.py:731
+msgid "Reports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:174 waeup/kofa/students/browser.py:2910
+msgid "Student Data Exports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:94
+msgid "Home"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:212
+msgid "Unique Identifier"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:213
+msgid ""
+"User Name, Student or Applicant Id, Matriculation or Registration Number"
+msgstr ""
+
+#: waeup/kofa/browser/layout.py:97
+msgid "'Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1009
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1011 waeup/kofa/browser/pages.py:2385
+#: waeup/kofa/students/browser.py:491
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1019
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1033
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1041
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1052 waeup/kofa/browser/pages.py:1059
+msgid "Your file contains forbidden characters. Please replace."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1169 waeup/kofa/browser/pages.py:1199
+#: waeup/kofa/browser/pages.py:1302 waeup/kofa/browser/pages.py:1415
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1183 waeup/kofa/browser/pages.py:1267
+#: waeup/kofa/browser/pages.py:1391
+msgid "Import aborted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:119
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1202
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1203
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1246
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1254
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1279
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1305 waeup/kofa/browser/pages.py:1534
+msgid "Reset"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1306
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1307
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1308
+msgid "Perform import"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:132 waeup/kofa/browser/pages.py:899
+#: waeup/kofa/hostels/browser.py:290 waeup/kofa/hostels/browser.py:320
+#: waeup/kofa/students/browser.py:333
+msgid "No item selected."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1373
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1417
+msgid "Process next"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1467
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1469
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1477
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1479
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1480
+msgid "Show"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1496
+msgid "Back"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1497
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1505
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1520
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1523
+msgid "No search results found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1531
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1551
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1554
+msgid "Given storage path cannot be used."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1555
+msgid "Error: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1558
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1570
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1572
+msgid "Create CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1597 waeup/kofa/students/browser.py:2923
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1689
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1701 waeup/kofa/browser/pages.py:1720
+#: waeup/kofa/browser/pages.py:1737 waeup/kofa/browser/pages.py:1741
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1702 waeup/kofa/university/export.py:36
+msgid "Faculties"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1706 waeup/kofa/browser/viewlets.py:419
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1715
+msgid "You are not allowed to remove entire faculties."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1748
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1773 waeup/kofa/browser/pages.py:1783
+#: waeup/kofa/university/export.py:78
+msgid "Departments"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1785 waeup/kofa/browser/pages.py:1833
+#: waeup/kofa/browser/pages.py:1852 waeup/kofa/browser/pages.py:1856
+msgid "Add department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1793 waeup/kofa/browser/viewlets.py:446
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1819
+msgid "You are not allowed to remove entire departments."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1863
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1866
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1888
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1971
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1986
+#: waeup/kofa/browser/pages.py:2015 waeup/kofa/browser/pages.py:2019
+msgid "Add course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1920 waeup/kofa/browser/pages.py:1977
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1920 waeup/kofa/browser/pages.py:1991
+#: waeup/kofa/browser/pages.py:2056 waeup/kofa/browser/pages.py:2060
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1929 waeup/kofa/browser/viewlets.py:453
+msgid "Manage department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1980
+msgid "You are not allowed to remove certificates."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2031
+msgid "A course with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2037
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2072
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2078
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2113 waeup/kofa/browser/viewlets.py:483
+msgid "Edit course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2155
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2162 waeup/kofa/browser/pages.py:2191
+#: waeup/kofa/browser/pages.py:2228 waeup/kofa/browser/pages.py:2230
+msgid "Add certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2163 waeup/kofa/browser/pages.py:2186
+msgid "Remove selected certificate courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2169 waeup/kofa/browser/viewlets.py:513
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2235
+msgid "The chosen certificate course is already part of this certificate."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2239
+msgid "certificate course ${a}_${b} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2280 waeup/kofa/browser/viewlets.py:543
+msgid "Edit certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2283
+msgid "Save and return"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2316
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2364
+msgid "No record found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2378 waeup/kofa/students/browser.py:2748
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2382
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:246 waeup/kofa/browser/viewlets.py:281
+#: waeup/kofa/browser/templates/staffsitelayout.pt:28
+#: waeup/kofa/browser/templates/studentsitelayout.pt:32
+msgid "Login"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:257
+msgid "You logged in."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:266
+msgid "Your personal data record is outdated. Please update."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:300
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:307
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:309
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:324
+msgid "You have been logged out. Thanks for using WAeUP Kofa!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:365
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:371 waeup/kofa/browser/pages.py:652
+msgid "Send message now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:390 waeup/kofa/browser/pages.py:423
+#: waeup/kofa/browser/pages.py:669 waeup/kofa/students/browser.py:489
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:410
+msgid "Send now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:425
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:450
+msgid "<h1>Welcome to WAeUP.Kofa</h1>"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:508 waeup/kofa/hostels/browser.py:228
+#: waeup/kofa/students/viewlets.py:168 waeup/kofa/students/viewlets.py:252
+#: waeup/kofa/students/viewlets.py:321 waeup/kofa/students/viewlets.py:337
+#: waeup/kofa/students/viewlets.py:396 waeup/kofa/students/viewlets.py:463
+msgid "Manage"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:509 waeup/kofa/browser/viewlets.py:190
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:517
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:554 waeup/kofa/browser/pages.py:556
+#: waeup/kofa/browser/viewlets.py:549
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:578
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:599
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:633
+msgid "User settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:650 waeup/kofa/students/browser.py:470
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:679 waeup/kofa/browser/viewlets.py:369
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:692 waeup/kofa/browser/viewlets.py:385
+msgid "My Roles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:723
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:750
+msgid "View portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:763
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:764 waeup/kofa/browser/pages.py:814
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:766 waeup/kofa/browser/pages.py:793
+#: waeup/kofa/browser/pages.py:826
+msgid "Add session configuration"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:805
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:817
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:830
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:841
+msgid "The session chosen already exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:863
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:910
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:913
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:924
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:935
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:938
+msgid "Upload"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:984
+msgid "Import Manager"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:986
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:99
+msgid "No local role selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:990
+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/pdf.py:153
+msgid "Date:"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:204 waeup/kofa/browser/pdf.py:461
+#: waeup/kofa/browser/pdf.py:471
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:12
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:16
+msgid "Date"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:522
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:791
+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
+#: waeup/kofa/students/browser_templates/exportconfig_course.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:6
+msgid "Certificate Courses"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:29
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:35
+#: waeup/kofa/students/browser_templates/transcript.pt:36
+msgid "Semester"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:36
+msgid "Cert. Course"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:37
+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:8
+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:34
+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/datacenterexportpage.pt:44
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:4
+msgid "Export Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:45
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:5
+msgid "Exporter"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:46
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:6
+msgid "Filter"
+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: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:16
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:18
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:41
+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:24
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:13
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:37
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:15
+msgid "Header"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:38
+msgid "change to"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:39
+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:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:14
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:36
+msgid "Reload page"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:12
+msgid "Overwrite contents in new location?"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:3
+#: waeup/kofa/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:7
+msgid "Copy existing data to new storage?"
+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:18
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:14
+msgid "Datasets"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:19
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:11
+msgid "Upload Date"
+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:113
+msgid "Download"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:22
+msgid "Processor:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:35
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:59
+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:67
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:72
+msgid "Processor"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:73
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:74
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:75
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:76
+msgid "CSV Skeleton File"
+msgstr ""
+
+#: waeup/kofa/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:101
+#: waeup/kofa/browser/templates/facultymanagepage.pt:75
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:97
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:112
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:91
+msgid "Local Role"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:16
+#: waeup/kofa/browser/templates/userscontainerpage.pt:7
+#: waeup/kofa/browser/templates/facultymanagepage.pt:16
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:17
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:16
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:6
+#: waeup/kofa/browser/templates/facultymanagepage.pt:6
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:5
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:7
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:9
+msgid "Settings"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:99
+#: waeup/kofa/browser/templates/userscontainerpage.pt:5
+#: waeup/kofa/browser/templates/facultymanagepage.pt:73
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:110
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:89
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:22
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:39
+msgid "Name"
+msgstr ""
+
+#: waeup/kofa/browser/templates/facultypage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/kofa/browser/templates/fileupload.pt:26
+#: waeup/kofa/students/browser_templates/imageupload.pt:20
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:32
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:13
+#: waeup/kofa/browser/templates/usereditformpage.pt:9
+#: waeup/kofa/students/browser_templates/basepage.pt:14
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:18
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:27
+#: waeup/kofa/students/browser_templates/change_password.pt:10
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:31
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:27
+msgid "Password:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:24
+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:36
+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:39
+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:44
+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:48
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:5
+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/reportscontainercreate.pt:22
+msgid "Type of report to create"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:41
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:76
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:36
+msgid "<img src=\"${DYNAMIC_CONTENT}\" /> Reload"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:51
+msgid "Create new report"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:6
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:47
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:7
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:15
+msgid "Creator"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:7
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:48
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:91
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:47
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:16
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:17
+msgid "Department"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:17
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:46
+msgid "Type"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:9
+#: waeup/kofa/students/browser_templates/containerpage.pt:24
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:18
+msgid "Search Results"
+msgstr ""
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:10
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:62
+msgid "Comment"
+msgstr ""
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:5
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:57
+msgid "Fullname"
+msgstr ""
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:6
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:8
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:49
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:58
+msgid "Status"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:113
+#: waeup/kofa/browser/templates/studentsitelayout.pt:97
+msgid "Back to bottom"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:122
+#: waeup/kofa/browser/templates/studentsitelayout.pt:107
+msgid "Back to top"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:124
+#: waeup/kofa/browser/templates/studentsitelayout.pt:109
+msgid "Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:127
+#: waeup/kofa/browser/templates/studentsitelayout.pt:112
+msgid "Copyright &copy; WAeUP Group 2008-2013"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:34
+#: waeup/kofa/browser/templates/studentsitelayout.pt:38
+msgid "Contact"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:4
+#: waeup/kofa/browser/templates/studentsitelayout.pt:4
+msgid "WAeUP.Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:40
+#: waeup/kofa/browser/templates/studentsitelayout.pt:44
+msgid "Logout"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:62
+msgid "Student Id:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:66
+msgid "State:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/usereditformpage.pt:15
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:24
+#: waeup/kofa/students/browser_templates/change_password.pt:16
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:44
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:180
+msgid "Add"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:320
+msgid "Any link text"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:402 waeup/kofa/students/viewlets.py:160
+msgid "Send email"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:411
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:428 waeup/kofa/browser/viewlets.py:474
+#: waeup/kofa/browser/viewlets.py:504 waeup/kofa/browser/viewlets.py:534
+msgid "Export student data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:437
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:463 waeup/kofa/browser/viewlets.py:493
+#: waeup/kofa/browser/viewlets.py:523
+msgid "Show students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:558
+msgid "Show logs"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:567
+msgid "Upload data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:576
+msgid "Process data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:585
+msgid "Export data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:594
+msgid "View processed files"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:667
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:95 waeup/kofa/browser/viewlets.py:170
+#: waeup/kofa/students/viewlets.py:329 waeup/kofa/students/viewlets.py:536
+#: waeup/kofa/students/viewlets.py:583
+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:163
+#: waeup/kofa/hostels/browser.py:190
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:133 waeup/kofa/hostels/browser.py:168
+msgid "Clear all hostels"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:171
+msgid "All hostels cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:179
+msgid "Settings have been saved."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:193
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:202
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:204
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:239
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:242 waeup/kofa/hostels/browser.py:270
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:243 waeup/kofa/hostels/browser.py:284
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:244 waeup/kofa/hostels/browser.py:314
+msgid "Release selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:245 waeup/kofa/hostels/browser.py:341
+msgid "Clear hostel"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:275
+msgid ""
+"${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:308
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:332
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:337
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:344
+msgid "Hostel cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:357
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:363
+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/containerpage.pt:21
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:37
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:45
+msgid "Id"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:23
+msgid "Booked Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:47
+msgid "Number"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:6
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:66
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelpage.pt:5
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:19
+msgid "Reserved Beds:"
+msgstr ""
+
+#: waeup/kofa/hostels/export.py:61
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:9
+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:105
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:111
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:117
+msgid "Reserved Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:128
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:135
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:142
+msgid "Beds for Pre-Study Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:149
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:156
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:163
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:170
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:177
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:188
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:198
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:220
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:226
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:232
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:237
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:238
+msgid "Enter valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:253
+msgid ""
+"Either student does not exist or student is not in accommodation session."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/interfaces.py:260
+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:93
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:99
+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 K"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:63
+msgid "Block L"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:64
+msgid "Block M"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:65
+msgid "Block N"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:66
+msgid "Block O"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:67
+msgid "Block P"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:68
+msgid "Block Q"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:69
+msgid "Block R"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:70
+msgid "Block S"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:71
+msgid "Block T"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:72
+msgid "Block U"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:73
+msgid "Block V"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:74
+msgid "Block W"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:207
+msgid "Subject"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:211 waeup/kofa/students/browser.py:1134
+#: waeup/kofa/students/browser.py:1297
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:10
+msgid "Grade"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:368
+msgid "Processor name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:394
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:401
+msgid "Email to:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:408
+msgid "Subject:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:412
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:416
+msgid "Text:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:445 waeup/kofa/interfaces.py:501
+msgid "Public Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:455 waeup/kofa/interfaces.py:456
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:462
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:463
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:492
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:97
+#: waeup/kofa/browser/templates/userscontainerpage.pt:4
+#: waeup/kofa/browser/templates/facultymanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:93
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:108
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:87
+msgid "User Id"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:497
+#: waeup/kofa/students/browser_templates/containerpage.pt:36
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:42
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:48
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:29
+msgid "Full Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:507
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:524
+#: waeup/kofa/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:571
+msgid "Name of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:572
+msgid "Sample University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:577
+msgid "Abbreviated Title of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:583
+msgid "Skin"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:59
+msgid "new"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:590
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:60
+msgid "completed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:602
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:608
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:61
+msgid "pending"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:615
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:616
+msgid "Kofa Contact"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:62
+msgid "active"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:621
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:628
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:63
+msgid "assigned"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:635
+msgid "Carry-over Course Registration"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:64
+msgid "callbacks"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:644 waeup/kofa/interfaces.py:690
+msgid "Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:658
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:664
+msgid "Bed Booking Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:670
+msgid "Rent"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:676
+msgid "Clearance enabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:92
+msgid "clearance started"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:93
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:94
+msgid "cleared"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:95
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:96
+msgid "courses registered"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:97
+msgid "courses validated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:98
+msgid "returning"
+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. Proceed to the login page and enter your "
+"credentials."
+msgstr ""
+
+#: waeup/kofa/objecthistory.py:74
+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:782
+#: waeup/kofa/students/interfaces.py:806
+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:789
+#: waeup/kofa/students/interfaces.py:813
+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/applicants/browser_templates/applicanteditpage.pt:92
+#: 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:853
+msgid "System"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1024
+msgid "You must select a session for the level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1033
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1035 waeup/kofa/students/browser.py:2477
+msgid "This level exists."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1045
+msgid "No study level selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1073
+msgid "You are not allowed to view the transcript."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1088
+msgid "${a}: Transcript Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1106
+msgid "You are not allowed to download the transcript."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1121 waeup/kofa/students/viewlets.py:366
+msgid "Academic Transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1125
+msgid "CERTIFIED TRUE COPY"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1129 waeup/kofa/students/browser.py:1289
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:35
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:25
+msgid "Sem."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1132 waeup/kofa/students/interfaces.py:623
+#: waeup/kofa/university/interfaces.py:152
+#: waeup/kofa/students/browser_templates/transcript.pt:39
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:19
+msgid "Credits"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1133 waeup/kofa/students/browser.py:1296
+#: waeup/kofa/students/interfaces.py:634 waeup/kofa/students/interfaces.py:667
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:20
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:32
+msgid "Score"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1161 waeup/kofa/students/viewlets.py:203
+msgid "Transfer student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1171
+msgid "Transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1175
+msgid "Current level does not match certificate levels."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1177
+msgid "Former study course record incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1179
+msgid "Maximum number of transfers exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1181
+msgid "Successfully transferred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1191
+msgid "Revert previous transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1196
+msgid "No previous transfer."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1201
+msgid "Revert now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1204
+msgid "Previous transfer reverted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1235
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1256
+msgid "Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1262
+msgid "1st Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1268
+msgid "2nd Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1274
+msgid "Level Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1283
+msgid "Course Registration Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1292
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:38
+msgid "Dept."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1293
+#: waeup/kofa/browser/templates/searchpage.pt:15
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:22
+msgid "Faculty"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1294
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:40
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:30
+msgid "Cred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1342 waeup/kofa/students/browser.py:1481
+#: waeup/kofa/students/browser.py:1492 waeup/kofa/students/browser.py:2602
+msgid "Add course ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1360 waeup/kofa/students/browser.py:2507
+msgid "No valid course code entered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1366 waeup/kofa/students/browser.py:2512
+msgid "Course not found."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1383
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1397 waeup/kofa/students/browser.py:2536
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:141
+msgid "Clearance is disabled for this session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1427 waeup/kofa/students/browser.py:1452
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1431
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1433 waeup/kofa/students/browser.py:1464
+msgid "Student is in the wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1457
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1461
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1516
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:152
+msgid "Total credits exceed ${a}."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1541
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1574
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:159
+msgid "The ticket exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:161
+msgid "Successfully added ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1622
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1630 waeup/kofa/students/browser.py:1680
+#: waeup/kofa/students/browser.py:1711
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1640 waeup/kofa/students/browser.py:1931
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1646
+msgid "You have not yet booked accommodation."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1670
+msgid "Add previous session online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1675
+msgid "No previous payment to be made."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1708
+msgid "Add balance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:182 waeup/kofa/students/export.py:197
+#: waeup/kofa/students/reports.py:154 waeup/kofa/students/viewlets.py:688
+msgid "Students"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1844
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1856
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1863
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1895
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1897
+msgid "Create bed ticket"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1914
+msgid "Outside booking period: ${a} - ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1917
+msgid "Outside booking period."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1921
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1925
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1936
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1949 waeup/kofa/students/browser.py:2268
+#: waeup/kofa/students/browser.py:2410
+msgid "Activation code is invalid."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1961 waeup/kofa/students/browser.py:1976
+msgid "System error: Please contact the adminsitrator."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1981 waeup/kofa/students/browser.py:2117
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1987 waeup/kofa/students/browser.py:2271
+#: waeup/kofa/students/browser.py:2414
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1990 waeup/kofa/students/browser.py:2275
+#: waeup/kofa/students/browser.py:2417 waeup/kofa/students/browser.py:2662
+msgid "invalidated"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1995 waeup/kofa/students/browser.py:2279
+#: waeup/kofa/students/browser.py:2422
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2007 waeup/kofa/students/browser.py:2131
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2016
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2033
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2052
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:206 waeup/kofa/students/viewlets.py:119
+#: waeup/kofa/students/viewlets.py:751
+msgid "Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2061
+msgid "Bed Allocation Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:208
+msgid "Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2089
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2095
+msgid "Student can't be relocated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2139
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:214 waeup/kofa/students/viewlets.py:125
+#: waeup/kofa/students/viewlets.py:752
+msgid "Payments"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2158
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2170
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2185 waeup/kofa/students/viewlets.py:545
+msgid "Change password"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2199
+msgid "Password changed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2211
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2226 waeup/kofa/students/viewlets.py:569
+#: waeup/kofa/students/workflow.py:48
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2230
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2248
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2252
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2256
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2283
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:229 waeup/kofa/students/viewlets.py:131
+msgid "Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2293
+msgid "Edit clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2314 waeup/kofa/students/browser.py:2328
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2322
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2339 waeup/kofa/students/browser.py:2373
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2342 waeup/kofa/students/browser.py:2376
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2351 waeup/kofa/students/workflow.py:62
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2352
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2355
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2367
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:238
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2385
+msgid "Start session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2389
+msgid "Start now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2398
+msgid "You are not entitled to start session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2435
+msgid "An error occurred, please contact the system administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2437
+msgid "Session started."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2456
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2468
+msgid "Create course list now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2479
+msgid "Your data are incomplete"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2524
+msgid "Edit course list of ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2565
+msgid "You are a postgraduate student, your course list can't bee registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:257
+msgid "Student Section"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2573
+msgid "Maximum credits of ${a} exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2578
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2582
+msgid "Register course list"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2618
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2621
+msgid "Set"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2636
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2644
+msgid "Access code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2647
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2653
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2659
+msgid "Access code has already been used."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2668
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2680
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2723 waeup/kofa/students/browser.py:2734
+msgid "No student record found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2768
+msgid "Your password request was successful."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2784 waeup/kofa/students/interfaces.py:417
+#: waeup/kofa/students/interfaces.py:508
+#: waeup/kofa/browser/templates/showstudentspage.pt:8
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:60
+msgid "Current Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2785 waeup/kofa/students/interfaces.py:424
+#: waeup/kofa/students/interfaces.py:456 waeup/kofa/students/interfaces.py:514
+#: waeup/kofa/browser/templates/showstudentspage.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:61
+msgid "Current Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:282
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2836
+msgid "in any session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2842
+msgid "at any level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:287 waeup/kofa/students/browser.py:331
+#: waeup/kofa/students/browser.py:2633 waeup/kofa/students/browser.py:2845
+msgid "No student found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2878 waeup/kofa/students/interfaces.py:539
+#: waeup/kofa/students/interfaces.py:714 waeup/kofa/students/utils.py:224
+#: waeup/kofa/students/browser_templates/transcript.pt:35
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:8
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:14
+msgid "Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2879 waeup/kofa/students/utils.py:226
+#: waeup/kofa/university/interfaces.py:340
+#: waeup/kofa/browser/templates/certificatepage.pt:28
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:34
+#: waeup/kofa/students/browser_templates/transcript.pt:34
+msgid "Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2936
+msgid "Configure student data export"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2944
+msgid "All Sessions"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2951
+msgid "All Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2958
+msgid "All Modes"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:298 waeup/kofa/students/viewlets.py:144
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3022
+msgid "Export started for students with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3113
+msgid "Export started for course tickets with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:358 waeup/kofa/students/viewlets.py:151
+msgid "Add student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:366
+msgid "Student record created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:380
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:402
+msgid "Login now"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:406
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:412
+msgid "You successfully logged in as student."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:430
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:432
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:509
+msgid "Admission Letter of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:527
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:576
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:616
+msgid "Student account has been activated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:635
+msgid "Student account has been deactivated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:670
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:700 waeup/kofa/students/viewlets.py:109
+#: waeup/kofa/students/viewlets.py:749
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:6
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:706
+msgid "Clearance Slip of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:715
+msgid "Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:716
+msgid "Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:723
+msgid "Date, Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:724
+msgid "Date, Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:745
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:789
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:791 waeup/kofa/students/browser.py:826
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:803 waeup/kofa/students/viewlets.py:277
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:815
+msgid "Save comment and reject clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:819
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:823
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:856
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:869
+msgid "Manage personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:884
+msgid "Edit personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:887
+msgid "Save/Confirm"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:914
+msgid "${a}: Study Course"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:917
+msgid "${a}: Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:945
+msgid "First Study Course, "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:947
+msgid "Second Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:952
+msgid "First Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:963
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:966 waeup/kofa/students/browser.py:1039
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:967 waeup/kofa/students/browser.py:1019
+msgid "Add study level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:999
+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/clearanceeditpage.pt:8
+msgid "Scans"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:31
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:38
+msgid "Select all"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:10
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:10
+msgid "with registration number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:11
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:11
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:12
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:12
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:78
+msgid "in session"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:13
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:13
+msgid "in department"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:14
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:14
+msgid "in study mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:15
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:15
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:33
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:27
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:34
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:40
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:35
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:41
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:15
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:25
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "State"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:5
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:5
+msgid "for students"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:8
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:8
+msgid "with id"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:9
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:9
+msgid "with name"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:18
+msgid "Weight"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:20
+msgid "The activation code can be found on the payment slip."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:34
+msgid ""
+"Your data have been approved. You can continue by clicking the button below."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:48
+msgid "Current Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig.pt:1
+msgid ""
+"<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."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:12
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:2
+#: waeup/kofa/students/browser_templates/exportconfig.pt:12
+msgid "Exporter (Data Type):"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:24
+#: waeup/kofa/students/browser_templates/exportconfig.pt:24
+msgid "Current Session:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:36
+#: waeup/kofa/students/browser_templates/exportconfig.pt:36
+msgid "Current Level:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:14
+msgid "Level Session:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:50
+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:93
+#: 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:94
+#: 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:90
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:46
+msgid "Payment Id"
+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: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:32
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:33
+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/studentsreportgeneratorpage.pt:10
+msgid "Study Modes Group:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.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/browser_templates/studentsreportgeneratorpage.pt:21
+msgid "Set study modes filter."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.pt:33
+msgid "<option value=\"${DYNAMIC_CONTENT}\"> ${DYNAMIC_CONTENT} </option>"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.pt:37
+msgid "Select academic session to report."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:6
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:9
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:50
+msgid "Previous Study Courses:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:64
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:30
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:69
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:35
+msgid "Level Code"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:70
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:36
+msgid "Level Title"
+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: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:39
+msgid "Fact."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:31
+msgid "Requ."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:43
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:21
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:34
+msgid "CO"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:44
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:35
+msgid "Auto"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:6
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:87
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:62
+msgid "Enter valid course code."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:93
+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:28
+msgid "Dep."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:29
+msgid "Fac."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:58
+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/export.py:220
+msgid "Student Study Courses"
+msgstr ""
+
+#: waeup/kofa/students/export.py:250
+msgid "Student Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/export.py:275
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:9
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:30
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:20
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:309
+msgid "Student Payments"
+msgstr ""
+
+#: waeup/kofa/students/export.py:346
+msgid "Payment Data for Bursary"
+msgstr ""
+
+#: waeup/kofa/students/export.py:377
+msgid "Bed Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:413
+msgid "Student Payments Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:442
+msgid "Student Study Levels Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:471
+msgid "Combo Card Data"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:191
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:193
+msgid ""
+"This message will be shown if and only if deactivated students try to login."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:232 waeup/kofa/students/interfaces.py:365
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:239
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:245
+msgid "Email"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:280
+msgid "Officer's Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:285
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:291
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:304
+msgid "Nationality"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:313
+msgid "Employer"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:323
+msgid "Updated"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:329 waeup/kofa/students/interfaces.py:347
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:376
+msgid "Registr. or Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:396 waeup/kofa/students/interfaces.py:450
+#: waeup/kofa/browser/templates/showstudentspage.pt:7
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:59
+msgid "Certificate"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:402 waeup/kofa/students/interfaces.py:482
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:409 waeup/kofa/students/interfaces.py:463
+#: waeup/kofa/students/interfaces.py:489
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:431 waeup/kofa/students/interfaces.py:502
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:438
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:520
+msgid "Bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:525 waeup/kofa/students/interfaces.py:552
+msgid "Validated by"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:545
+msgid "Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:558
+msgid "Validation Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:564
+msgid "Total Credits"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:602
+msgid "Faculty Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:607
+msgid "Department Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:612
+#: waeup/kofa/university/interfaces.py:164
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:618
+#: waeup/kofa/university/interfaces.py:158
+msgid "Passmark"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:628
+#: waeup/kofa/browser/templates/certificatepage.pt:33
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:39
+msgid "Required"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:640
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:646
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:657
+#: waeup/kofa/university/interfaces.py:334
+#: waeup/kofa/university/interfaces.py:369
+#: waeup/kofa/students/browser_templates/transcript.pt:37
+msgid "Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:673
+msgid "Level Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:702
+msgid "Allocated Bed"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:708
+msgid "Requested Bed Type"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:721
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:727
+msgid "Booking Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:748
+msgid "Current Session Payment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:754 waeup/kofa/students/interfaces.py:795
+#: waeup/kofa/students/interfaces.py:819
+msgid "Payment Level"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:825
+msgid "Balance Amount"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:829
+msgid "Balance in Naira "
+msgstr ""
+
+#: waeup/kofa/students/reports.py:176
+msgid "Create students report"
+msgstr ""
+
+#: waeup/kofa/students/reports.py:196
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:122
+msgid "Name:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:141
+msgid "Study Course:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:148 waeup/kofa/students/utils.py:734
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:157 waeup/kofa/students/utils.py:727
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:166 waeup/kofa/students/utils.py:741
+msgid "Entry Session:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:174
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:261
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:338
+msgid "The previous session must not fall below your entry session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:343 waeup/kofa/students/utils.py:346
+msgid "This is not a previous session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:363 waeup/kofa/students/utils.py:402
+msgid "Study course data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:417
+msgid "no bed allocated"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:419
+msgid "Amount could not be determined."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:452
+msgid "Amount must be greater than 0."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:516
+msgid ""
+"This is to inform you that you have been provisionally admitted into ${a} as "
+"follows:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:556
+msgid "Your Kofa student record was created on ${a}."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:608 waeup/kofa/students/viewlets.py:76
+#: waeup/kofa/students/viewlets.py:104
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:7
+msgid "Base Data"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:628
+msgid "Workflow History"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:721
+msgid "Course of Study:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:748
+msgid "Entry Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:755
+msgid "Cumulative GPA:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:786
+msgid "Error in image file."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:814
+msgid "(not yet)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:114 waeup/kofa/students/viewlets.py:750
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:136 waeup/kofa/students/viewlets.py:755
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:27
+msgid "History"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:177
+msgid "Trigger transition"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:186
+msgid "Login as student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:195
+msgid "Download admission letter"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:212
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:224 waeup/kofa/students/viewlets.py:244
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:232
+msgid "Activate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:260 waeup/kofa/students/workflow.py:76
+msgid "Clear student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:295
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:304 waeup/kofa/students/viewlets.py:313
+msgid "View"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:351
+msgid "Transcript"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:382
+msgid "Reactivate this study course (revert previous transfer)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:411 waeup/kofa/students/workflow.py:160
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:429
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:448
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:510
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:519
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:528
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:554
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:598
+msgid "Start new session"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:612
+msgid "Add course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:631
+msgid "Edit course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:649
+msgid "Add current session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:657
+msgid "Add previous session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:672
+msgid "Add balance payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:754
+msgid "Accommodation Data"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:766
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:780
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:786
+msgid "Could not determine file type."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:791
+msgid "${a} file extension expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:797
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:806
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:824
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:10
+#: waeup/kofa/browser/templates/datacenterpage.pt:17
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:11
+msgid "File"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:825
+msgid "Scan"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:848
+msgid "Upload new file"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:849
+msgid "Delete attachment"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:886
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:90
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:900
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:9
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:911
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:926 waeup/kofa/students/viewlets.py:937
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:927 waeup/kofa/students/viewlets.py:938
+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: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:10
+#: waeup/kofa/browser/templates/departmentpage.pt:3
+#: waeup/kofa/browser/templates/certificatepage.pt:22
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:28
+msgid "Courses"
+msgstr ""
+
+#: waeup/kofa/university/export.py:165
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:13
+#: 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:146
+msgid "Title of course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:171
+msgid "Former Course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:172
+msgid ""
+"If this attribute is being set all certificate courses referring to this "
+"course will be automatically deleted."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:189
+msgid "Enter unique course code which will become part of the URL."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:209
+msgid "Unnamed Certificate"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:214
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:221
+msgid "Start Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:228
+msgid "End Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:235
+msgid "Application Category"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:242
+msgid "Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:247
+msgid "Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:252
+msgid "Foreigner Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:257
+msgid "Foreigner Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:262
+msgid "Installment Ratio"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:269
+msgid "Custom Textline 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:274
+msgid "Custom Textline 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:279
+msgid "Custom Float 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:284
+msgid "Custom Float 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:296
+#: waeup/kofa/university/interfaces.py:298
+msgid "Start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:300
+#: waeup/kofa/university/interfaces.py:303
+msgid "Study mode, start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:313
+msgid "Enter unique certificate code which will become part of the URL."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:347
+msgid "Registration required"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:45
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:46
+#: waeup/kofa/university/interfaces.py:94
+#: waeup/kofa/university/interfaces.py:147
+msgid "Unnamed"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:51
+#: waeup/kofa/university/interfaces.py:99
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:67
+#: waeup/kofa/university/interfaces.py:118
+msgid "This code will become part of the URL."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:93
+msgid "Name of department"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:29
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:30
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:31
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:32
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:33
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:34
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:35
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:36
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:37
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:38
+msgid "900 (Year 9)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:39
+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:186
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:239
+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:18
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/kofa/widgets/sequencewidget.pt:21
+msgid "remove-selected-items"
+msgstr ""
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/ig/LC_MESSAGES/waeup.kofa.po
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/ig/LC_MESSAGES/waeup.kofa.po	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/ig/LC_MESSAGES/waeup.kofa.po	(revision 10886)
@@ -0,0 +1,6189 @@
+# #############################################################################
+#
+# 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: Thu Jun 27 09:36:46 2013\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:102 waeup/kofa/accesscodes/browser.py:115
+#: waeup/kofa/applicants/browser.py:262 waeup/kofa/applicants/browser.py:351
+#: waeup/kofa/applicants/browser.py:352 waeup/kofa/applicants/browser.py:472
+#: waeup/kofa/browser/pages.py:636 waeup/kofa/browser/pages.py:846
+#: waeup/kofa/browser/pages.py:872 waeup/kofa/browser/pages.py:939
+#: waeup/kofa/browser/pages.py:1171 waeup/kofa/browser/pages.py:1201
+#: waeup/kofa/browser/pages.py:1304 waeup/kofa/browser/pages.py:1535
+#: waeup/kofa/browser/pages.py:1701 waeup/kofa/browser/pages.py:1725
+#: waeup/kofa/browser/pages.py:1756 waeup/kofa/browser/pages.py:1784
+#: waeup/kofa/browser/pages.py:1785 waeup/kofa/browser/pages.py:1828
+#: waeup/kofa/browser/pages.py:1873 waeup/kofa/browser/pages.py:1918
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1921
+#: waeup/kofa/browser/pages.py:1996 waeup/kofa/browser/pages.py:2045
+#: waeup/kofa/browser/pages.py:2086 waeup/kofa/browser/pages.py:2122
+#: waeup/kofa/browser/pages.py:2161 waeup/kofa/browser/pages.py:2163
+#: waeup/kofa/browser/pages.py:2196 waeup/kofa/browser/pages.py:2247
+#: waeup/kofa/browser/pages.py:2304 waeup/kofa/students/browser.py:965
+#: waeup/kofa/students/browser.py:966 waeup/kofa/students/browser.py:1341
+#: waeup/kofa/students/browser.py:1343 waeup/kofa/students/browser.py:1500
+#: waeup/kofa/students/browser.py:1659 waeup/kofa/students/browser.py:1697
+#: waeup/kofa/students/browser.py:1730
+msgid "Cancel"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:104
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:114
+msgid "Reimport"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:117
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:122
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:128
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:138
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:141
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:154
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:155 waeup/kofa/applicants/browser.py:85
+#: waeup/kofa/applicants/browser.py:113 waeup/kofa/browser/pages.py:725
+#: waeup/kofa/browser/pages.py:1495 waeup/kofa/browser/viewlets.py:200
+#: waeup/kofa/students/browser.py:258 waeup/kofa/students/browser.py:299
+msgid "Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:156
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:157
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:182 waeup/kofa/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:184
+msgid "${a} disabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:188
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:192
+msgid "re-enabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:194
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:198
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:237
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:246
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:254
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:263
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:44
+msgid "Archive"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:45
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:47 waeup/kofa/accesscodes/browser.py:208
+#: waeup/kofa/accesscodes/browser.py:228
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:54
+msgid "No batch selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:62
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:70
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:79
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:85
+msgid "Create batch"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:93
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:95
+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:23
+msgid "Serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:24
+msgid "AC"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:26
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:48
+msgid "Owner"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:5
+msgid "for access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:7
+msgid "with PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:8
+msgid "with serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:9
+msgid "with message term"
+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:119
+msgid "Applicant has not yet been admitted."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:125
+msgid "Registration Number exists."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:128
+msgid "No course admitted provided."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/applicant.py:160
+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:1002
+msgid "1st and 2nd choice must be different."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1020
+msgid "The form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1026
+msgid "Form has been submitted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:1069
+msgid "Apply for ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1074
+msgid "Outside application period."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1089 waeup/kofa/browser/pages.py:2353
+#: waeup/kofa/students/browser.py:2700
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1112 waeup/kofa/students/browser.py:2717
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1118 waeup/kofa/applicants/browser.py:1130
+msgid "No application record found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:112 waeup/kofa/applicants/viewlets.py:90
+msgid "Search applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1122 waeup/kofa/students/browser.py:2726
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1141
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1158
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:128 waeup/kofa/students/browser.py:276
+#: waeup/kofa/students/browser.py:319
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:133
+msgid "No applicant found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:141 waeup/kofa/applicants/viewlets.py:99
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:144 waeup/kofa/applicants/browser.py:219
+#: waeup/kofa/applicants/browser.py:351 waeup/kofa/applicants/browser.py:404
+#: waeup/kofa/applicants/browser.py:795 waeup/kofa/applicants/browser.py:841
+#: waeup/kofa/applicants/browser.py:949 waeup/kofa/applicants/browser.py:952
+#: waeup/kofa/applicants/browser.py:996 waeup/kofa/browser/pages.py:607
+#: waeup/kofa/browser/pages.py:764 waeup/kofa/browser/pages.py:787
+#: waeup/kofa/browser/pages.py:866 waeup/kofa/browser/pages.py:1533
+#: waeup/kofa/browser/pages.py:1784 waeup/kofa/browser/pages.py:1824
+#: waeup/kofa/browser/pages.py:1918 waeup/kofa/browser/pages.py:1967
+#: waeup/kofa/browser/pages.py:2118 waeup/kofa/browser/pages.py:2161
+#: waeup/kofa/browser/pages.py:2182 waeup/kofa/hostels/browser.py:131
+#: waeup/kofa/hostels/browser.py:176 waeup/kofa/hostels/browser.py:241
+#: waeup/kofa/hostels/browser.py:265 waeup/kofa/hostels/browser.py:360
+#: waeup/kofa/students/browser.py:542 waeup/kofa/students/browser.py:595
+#: waeup/kofa/students/browser.py:770 waeup/kofa/students/browser.py:872
+#: waeup/kofa/students/browser.py:965 waeup/kofa/students/browser.py:993
+#: waeup/kofa/students/browser.py:1341 waeup/kofa/students/browser.py:1386
+#: waeup/kofa/students/browser.py:2173 waeup/kofa/students/browser.py:2188
+#: waeup/kofa/students/browser.py:2311
+msgid "Save"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:145 waeup/kofa/applicants/browser.py:175
+#: waeup/kofa/applicants/browser.py:352 waeup/kofa/applicants/browser.py:423
+#: waeup/kofa/browser/pages.py:767 waeup/kofa/browser/pages.py:809
+#: waeup/kofa/browser/pages.py:893 waeup/kofa/browser/pages.py:1701
+#: waeup/kofa/browser/pages.py:1712 waeup/kofa/browser/pages.py:1785
+#: waeup/kofa/browser/pages.py:1816 waeup/kofa/hostels/browser.py:134
+#: waeup/kofa/hostels/browser.py:149 waeup/kofa/students/browser.py:300
+#: waeup/kofa/students/browser.py:1840 waeup/kofa/students/browser.py:1853
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:145 waeup/kofa/applicants/browser.py:200
+#: waeup/kofa/applicants/browser.py:231 waeup/kofa/applicants/browser.py:241
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:146 waeup/kofa/applicants/browser.py:209
+#: waeup/kofa/applicants/browser.py:354 waeup/kofa/applicants/browser.py:481
+#: waeup/kofa/browser/pages.py:1786 waeup/kofa/browser/pages.py:1842
+#: waeup/kofa/browser/pages.py:1922 waeup/kofa/browser/pages.py:2005
+#: waeup/kofa/browser/pages.py:2164 waeup/kofa/browser/pages.py:2215
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:147 waeup/kofa/applicants/browser.py:205
+#: waeup/kofa/applicants/browser.py:355 waeup/kofa/applicants/browser.py:477
+#: waeup/kofa/browser/pages.py:1787 waeup/kofa/browser/pages.py:1838
+#: waeup/kofa/browser/pages.py:1923 waeup/kofa/browser/pages.py:2001
+#: waeup/kofa/browser/pages.py:2165 waeup/kofa/browser/pages.py:2211
+msgid "Add local role"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:148
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:181
+msgid "No container selected!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:192 waeup/kofa/applicants/browser.py:440
+#: waeup/kofa/applicants/browser.py:896
+msgid "Could not delete:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:195 waeup/kofa/applicants/browser.py:443
+#: waeup/kofa/applicants/browser.py:899 waeup/kofa/browser/pages.py:153
+#: waeup/kofa/students/browser.py:346 waeup/kofa/students/browser.py:1055
+#: waeup/kofa/students/browser.py:1407 waeup/kofa/students/browser.py:1602
+#: waeup/kofa/students/browser.py:1873 waeup/kofa/students/browser.py:2549
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:223 waeup/kofa/applicants/browser.py:418
+#: waeup/kofa/applicants/browser.py:869 waeup/kofa/applicants/browser.py:1005
+#: waeup/kofa/browser/pages.py:195 waeup/kofa/browser/pages.py:2295
+#: waeup/kofa/hostels/browser.py:55 waeup/kofa/students/browser.py:100
+#: waeup/kofa/students/browser.py:564
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:250
+msgid ""
+"An applicants container for the same application type and entrance year "
+"exists already in the database."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:258
+msgid "Added:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:270 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:353 waeup/kofa/applicants/browser.py:448
+msgid "Create students from selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:361
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:429 waeup/kofa/applicants/browser.py:454
+msgid "No applicant selected!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:465 waeup/kofa/applicants/browser.py:614
+msgid "${a} students successfully created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:468 waeup/kofa/applicants/browser.py:617
+msgid "No student could be created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:495 waeup/kofa/applicants/viewlets.py:115
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:498
+msgid "Create application record"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:503
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:517
+msgid "Applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:541 waeup/kofa/students/browser.py:438
+msgid "set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:542 waeup/kofa/students/browser.py:439
+msgid "unset"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:547
+msgid "${a} <br /> Application Record ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:637 waeup/kofa/students/utils.py:426
+#: waeup/kofa/students/utils.py:459
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:649 waeup/kofa/students/browser.py:1655
+#: waeup/kofa/students/browser.py:1693 waeup/kofa/students/browser.py:1726
+msgid "Payment ticket created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:672 waeup/kofa/students/browser.py:1749
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:68
+msgid ""
+"'You can not edit your application records after final submission. You "
+"really want to submit?'"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:717 waeup/kofa/students/browser.py:1806
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:723 waeup/kofa/students/browser.py:1812
+msgid "Online Payment Slip"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:749
+msgid "Please pay and submit before trying to download the application slip."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:770
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:775
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:795 waeup/kofa/applicants/browser.py:952
+#: waeup/kofa/applicants/browser.py:1008
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:796 waeup/kofa/applicants/browser.py:908
+#: waeup/kofa/applicants/browser.py:950 waeup/kofa/applicants/browser.py:985
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:796 waeup/kofa/applicants/browser.py:913
+#: waeup/kofa/applicants/browser.py:950 waeup/kofa/applicants/browser.py:953
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/students/browser.py:1343
+#: waeup/kofa/students/browser.py:1391 waeup/kofa/students/browser.py:1583
+#: waeup/kofa/students/browser.py:2557
+msgid "Remove selected tickets"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:826
+msgid "${a} <br /> Application Form ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:838 waeup/kofa/students/browser.py:592
+msgid "No transition"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:84
+msgid "Application Section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:883 waeup/kofa/students/browser.py:1589
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:961 waeup/kofa/students/browser.py:108
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:977
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:979
+msgid "Passport picture confirmation box not ticked."
+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:28
+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:25
+msgid "Passport Photo:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:50
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:66
+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:82
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:40
+msgid "Application Fee Payment Tickets"
+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/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:46
+msgid "Application Number"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:50
+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:92
+msgid "note_acmp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:93
+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:38
+msgid "note_armp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:56
+msgid "Records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:6
+msgid "Introduction"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:12
+msgid "Application Period"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:50
+msgid "Year"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:11
+msgid "with email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:5
+msgid "for applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/export.py:39
+msgid "Basic Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:143
+msgid "Create Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:144
+msgid "Update Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:174
+#: waeup/kofa/applicants/interfaces.py:238
+msgid "Human readable description in HTML format"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:192 waeup/kofa/students/browser.py:1130
+#: waeup/kofa/students/browser.py:1290 waeup/kofa/university/interfaces.py:38
+#: waeup/kofa/university/interfaces.py:66
+#: waeup/kofa/university/interfaces.py:86
+#: waeup/kofa/university/interfaces.py:117
+#: waeup/kofa/university/interfaces.py:139
+#: waeup/kofa/university/interfaces.py:188
+#: waeup/kofa/university/interfaces.py:201
+#: waeup/kofa/university/interfaces.py:311
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:39
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:68
+#: waeup/kofa/browser/templates/facultypage.pt:8
+#: waeup/kofa/browser/templates/departmentpage.pt:15
+#: waeup/kofa/browser/templates/departmentpage.pt:38
+#: waeup/kofa/browser/templates/searchpage.pt:13
+#: waeup/kofa/browser/templates/facultymanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:15
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:26
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:52
+msgid "Code"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:198 waeup/kofa/students/browser.py:1131
+#: waeup/kofa/students/browser.py:1291 waeup/kofa/students/interfaces.py:597
+#: waeup/kofa/university/interfaces.py:208
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:70
+#: waeup/kofa/browser/templates/facultypage.pt:9
+#: waeup/kofa/browser/templates/departmentpage.pt:16
+#: waeup/kofa/browser/templates/departmentpage.pt:39
+#: waeup/kofa/browser/templates/searchpage.pt:14
+#: waeup/kofa/browser/templates/certificatepage.pt:32
+#: waeup/kofa/browser/templates/facultymanagepage.pt:43
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:38
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:37
+#: waeup/kofa/students/browser_templates/transcript.pt:38
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:16
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:27
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:54
+msgid "Title"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:204
+#: waeup/kofa/applicants/interfaces.py:318
+msgid "Application Target"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:211
+#: waeup/kofa/applicants/interfaces.py:325
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:218
+msgid "Application Mode"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:232
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:251
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:253
+#: waeup/kofa/applicants/interfaces.py:259 waeup/kofa/hostels/interfaces.py:39
+#: waeup/kofa/hostels/interfaces.py:45
+msgid "Example:"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:257
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:263
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:269 waeup/kofa/interfaces.py:652
+msgid "Application Fee"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:275
+msgid "Hide container"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:353
+#: waeup/kofa/students/interfaces.py:185
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:359
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:26
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:364
+#: waeup/kofa/students/interfaces.py:225 waeup/kofa/students/interfaces.py:356
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:375
+#: waeup/kofa/applicants/interfaces.py:526
+#: waeup/kofa/students/interfaces.py:204 waeup/kofa/students/interfaces.py:381
+msgid "First Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:379
+#: waeup/kofa/students/interfaces.py:209
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:383
+#: waeup/kofa/students/interfaces.py:214
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:387
+#: waeup/kofa/students/interfaces.py:297
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:393
+#: waeup/kofa/students/interfaces.py:219
+msgid "Sex"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:398
+#: waeup/kofa/applicants/interfaces.py:474
+#: waeup/kofa/applicants/interfaces.py:531
+#: waeup/kofa/browser/interfaces.py:221 waeup/kofa/interfaces.py:435
+#: waeup/kofa/interfaces.py:511 waeup/kofa/students/interfaces.py:386
+msgid "Email Address"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:403 waeup/kofa/interfaces.py:440
+#: waeup/kofa/interfaces.py:518 waeup/kofa/students/interfaces.py:250
+msgid "Phone"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:408
+#: waeup/kofa/applicants/interfaces.py:479
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:413
+#: waeup/kofa/applicants/interfaces.py:484
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:425
+#: waeup/kofa/applicants/interfaces.py:495
+msgid "Notice"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:429
+#: waeup/kofa/students/interfaces.py:199
+#: waeup/kofa/browser/templates/showstudentspage.pt:4
+#: waeup/kofa/students/browser_templates/containerpage.pt:32
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:38
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:56
+msgid "Student Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:434
+#: waeup/kofa/applicants/interfaces.py:489
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:439
+msgid "Form locked"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:53 waeup/kofa/applicants/workflow.py:74
+#: waeup/kofa/students/payments.py:115
+msgid "Payment approved"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:66 waeup/kofa/students/payments.py:104
+msgid "Successful payment"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:73 waeup/kofa/students/payments.py:122
+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:40
+msgid "Desired Study Courses"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:41
+msgid "Process Data"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:56 waeup/kofa/students/browser.py:139
+#: waeup/kofa/students/utils.py:356 waeup/kofa/students/utils.py:383
+#: waeup/kofa/students/utils.py:394 waeup/kofa/students/utils.py:450
+msgid "Session configuration object is not available."
+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:146 waeup/kofa/applicants/viewlets.py:155
+msgid "Create students"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:163
+msgid "View application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:171
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:179
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:197
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:213 waeup/kofa/applicants/workflow.py:108
+#: waeup/kofa/students/browser.py:361
+msgid "Create student record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:230 waeup/kofa/students/viewlets.py:480
+msgid "Download payment slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:245 waeup/kofa/applicants/workflow.py:73
+#: waeup/kofa/students/viewlets.py:495 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:728
+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:91
+msgid "admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:44
+msgid "not admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:45 waeup/kofa/interfaces.py:90
+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:93
+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:652
+msgid "Academics"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:127 waeup/kofa/browser/pages.py:460
+msgid "Administration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:134 waeup/kofa/browser/viewlets.py:335
+#: waeup/kofa/browser/viewlets.py:703
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:152 waeup/kofa/browser/pages.py:507
+#: waeup/kofa/browser/viewlets.py:344 waeup/kofa/browser/viewlets.py:713
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:159 waeup/kofa/browser/pages.py:885
+#: waeup/kofa/browser/pages.py:1493 waeup/kofa/browser/viewlets.py:353
+#: waeup/kofa/browser/viewlets.py:722
+msgid "Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:166 waeup/kofa/browser/reports.py:37
+#: waeup/kofa/browser/viewlets.py:362 waeup/kofa/browser/viewlets.py:731
+msgid "Reports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:174 waeup/kofa/students/browser.py:2910
+msgid "Student Data Exports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:94
+msgid "Home"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:212
+msgid "Unique Identifier"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:213
+msgid ""
+"User Name, Student or Applicant Id, Matriculation or Registration Number"
+msgstr ""
+
+#: waeup/kofa/browser/layout.py:97
+msgid "'Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1009
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1011 waeup/kofa/browser/pages.py:2385
+#: waeup/kofa/students/browser.py:491
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1019
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1033
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1041
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1052 waeup/kofa/browser/pages.py:1059
+msgid "Your file contains forbidden characters. Please replace."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1169 waeup/kofa/browser/pages.py:1199
+#: waeup/kofa/browser/pages.py:1302 waeup/kofa/browser/pages.py:1415
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1183 waeup/kofa/browser/pages.py:1267
+#: waeup/kofa/browser/pages.py:1391
+msgid "Import aborted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:119
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1202
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1203
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1246
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1254
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1279
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1305 waeup/kofa/browser/pages.py:1534
+msgid "Reset"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1306
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1307
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1308
+msgid "Perform import"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:132 waeup/kofa/browser/pages.py:899
+#: waeup/kofa/hostels/browser.py:290 waeup/kofa/hostels/browser.py:320
+#: waeup/kofa/students/browser.py:333
+msgid "No item selected."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1373
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1417
+msgid "Process next"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1467
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1469
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1477
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1479
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1480
+msgid "Show"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1496
+msgid "Back"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1497
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1505
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1520
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1523
+msgid "No search results found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1531
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1551
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1554
+msgid "Given storage path cannot be used."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1555
+msgid "Error: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1558
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1570
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1572
+msgid "Create CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1597 waeup/kofa/students/browser.py:2923
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1689
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1701 waeup/kofa/browser/pages.py:1720
+#: waeup/kofa/browser/pages.py:1737 waeup/kofa/browser/pages.py:1741
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1702 waeup/kofa/university/export.py:36
+msgid "Faculties"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1706 waeup/kofa/browser/viewlets.py:419
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1715
+msgid "You are not allowed to remove entire faculties."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1748
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1773 waeup/kofa/browser/pages.py:1783
+#: waeup/kofa/university/export.py:78
+msgid "Departments"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1785 waeup/kofa/browser/pages.py:1833
+#: waeup/kofa/browser/pages.py:1852 waeup/kofa/browser/pages.py:1856
+msgid "Add department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1793 waeup/kofa/browser/viewlets.py:446
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1819
+msgid "You are not allowed to remove entire departments."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1863
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1866
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1888
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1971
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1986
+#: waeup/kofa/browser/pages.py:2015 waeup/kofa/browser/pages.py:2019
+msgid "Add course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1920 waeup/kofa/browser/pages.py:1977
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1920 waeup/kofa/browser/pages.py:1991
+#: waeup/kofa/browser/pages.py:2056 waeup/kofa/browser/pages.py:2060
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1929 waeup/kofa/browser/viewlets.py:453
+msgid "Manage department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1980
+msgid "You are not allowed to remove certificates."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2031
+msgid "A course with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2037
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2072
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2078
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2113 waeup/kofa/browser/viewlets.py:483
+msgid "Edit course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2155
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2162 waeup/kofa/browser/pages.py:2191
+#: waeup/kofa/browser/pages.py:2228 waeup/kofa/browser/pages.py:2230
+msgid "Add certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2163 waeup/kofa/browser/pages.py:2186
+msgid "Remove selected certificate courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2169 waeup/kofa/browser/viewlets.py:513
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2235
+msgid "The chosen certificate course is already part of this certificate."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2239
+msgid "certificate course ${a}_${b} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2280 waeup/kofa/browser/viewlets.py:543
+msgid "Edit certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2283
+msgid "Save and return"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2316
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2364
+msgid "No record found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2378 waeup/kofa/students/browser.py:2748
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2382
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:246 waeup/kofa/browser/viewlets.py:281
+#: waeup/kofa/browser/templates/staffsitelayout.pt:28
+#: waeup/kofa/browser/templates/studentsitelayout.pt:32
+msgid "Login"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:257
+msgid "You logged in."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:266
+msgid "Your personal data record is outdated. Please update."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:300
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:307
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:309
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:324
+msgid "You have been logged out. Thanks for using WAeUP Kofa!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:365
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:371 waeup/kofa/browser/pages.py:652
+msgid "Send message now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:390 waeup/kofa/browser/pages.py:423
+#: waeup/kofa/browser/pages.py:669 waeup/kofa/students/browser.py:489
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:410
+msgid "Send now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:425
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:450
+msgid "<h1>Welcome to WAeUP.Kofa</h1>"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:508 waeup/kofa/hostels/browser.py:228
+#: waeup/kofa/students/viewlets.py:168 waeup/kofa/students/viewlets.py:252
+#: waeup/kofa/students/viewlets.py:321 waeup/kofa/students/viewlets.py:337
+#: waeup/kofa/students/viewlets.py:396 waeup/kofa/students/viewlets.py:463
+msgid "Manage"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:509 waeup/kofa/browser/viewlets.py:190
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:517
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:554 waeup/kofa/browser/pages.py:556
+#: waeup/kofa/browser/viewlets.py:549
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:578
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:599
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:633
+msgid "User settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:650 waeup/kofa/students/browser.py:470
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:679 waeup/kofa/browser/viewlets.py:369
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:692 waeup/kofa/browser/viewlets.py:385
+msgid "My Roles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:723
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:750
+msgid "View portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:763
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:764 waeup/kofa/browser/pages.py:814
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:766 waeup/kofa/browser/pages.py:793
+#: waeup/kofa/browser/pages.py:826
+msgid "Add session configuration"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:805
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:817
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:830
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:841
+msgid "The session chosen already exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:863
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:910
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:913
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:924
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:935
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:938
+msgid "Upload"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:984
+msgid "Import Manager"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:986
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:99
+msgid "No local role selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:990
+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/pdf.py:153
+msgid "Date:"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:204 waeup/kofa/browser/pdf.py:461
+#: waeup/kofa/browser/pdf.py:471
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:12
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:16
+msgid "Date"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:522
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:791
+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
+#: waeup/kofa/students/browser_templates/exportconfig_course.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:6
+msgid "Certificate Courses"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:29
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:35
+#: waeup/kofa/students/browser_templates/transcript.pt:36
+msgid "Semester"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:36
+msgid "Cert. Course"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:37
+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:8
+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:34
+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/datacenterexportpage.pt:44
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:4
+msgid "Export Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:45
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:5
+msgid "Exporter"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:46
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:6
+msgid "Filter"
+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: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:16
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:18
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:41
+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:24
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:13
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:37
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:15
+msgid "Header"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:38
+msgid "change to"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:39
+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:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:14
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:36
+msgid "Reload page"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:12
+msgid "Overwrite contents in new location?"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:3
+#: waeup/kofa/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:7
+msgid "Copy existing data to new storage?"
+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:18
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:14
+msgid "Datasets"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:19
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:11
+msgid "Upload Date"
+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:113
+msgid "Download"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:22
+msgid "Processor:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:35
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:59
+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:67
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:72
+msgid "Processor"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:73
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:74
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:75
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:76
+msgid "CSV Skeleton File"
+msgstr ""
+
+#: waeup/kofa/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:101
+#: waeup/kofa/browser/templates/facultymanagepage.pt:75
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:97
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:112
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:91
+msgid "Local Role"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:16
+#: waeup/kofa/browser/templates/userscontainerpage.pt:7
+#: waeup/kofa/browser/templates/facultymanagepage.pt:16
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:17
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:16
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:6
+#: waeup/kofa/browser/templates/facultymanagepage.pt:6
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:5
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:7
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:9
+msgid "Settings"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:99
+#: waeup/kofa/browser/templates/userscontainerpage.pt:5
+#: waeup/kofa/browser/templates/facultymanagepage.pt:73
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:110
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:89
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:22
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:39
+msgid "Name"
+msgstr ""
+
+#: waeup/kofa/browser/templates/facultypage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/kofa/browser/templates/fileupload.pt:26
+#: waeup/kofa/students/browser_templates/imageupload.pt:20
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:32
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:13
+#: waeup/kofa/browser/templates/usereditformpage.pt:9
+#: waeup/kofa/students/browser_templates/basepage.pt:14
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:18
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:27
+#: waeup/kofa/students/browser_templates/change_password.pt:10
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:31
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:27
+msgid "Password:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:24
+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:36
+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:39
+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:44
+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:48
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:5
+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/reportscontainercreate.pt:22
+msgid "Type of report to create"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:41
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:76
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:36
+msgid "<img src=\"${DYNAMIC_CONTENT}\" /> Reload"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:51
+msgid "Create new report"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:6
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:47
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:7
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:15
+msgid "Creator"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:7
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:48
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:91
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:47
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:16
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:17
+msgid "Department"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:17
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:46
+msgid "Type"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:9
+#: waeup/kofa/students/browser_templates/containerpage.pt:24
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:18
+msgid "Search Results"
+msgstr ""
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:10
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:62
+msgid "Comment"
+msgstr ""
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:5
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:57
+msgid "Fullname"
+msgstr ""
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:6
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:8
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:49
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:58
+msgid "Status"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:113
+#: waeup/kofa/browser/templates/studentsitelayout.pt:97
+msgid "Back to bottom"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:122
+#: waeup/kofa/browser/templates/studentsitelayout.pt:107
+msgid "Back to top"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:124
+#: waeup/kofa/browser/templates/studentsitelayout.pt:109
+msgid "Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:127
+#: waeup/kofa/browser/templates/studentsitelayout.pt:112
+msgid "Copyright &copy; WAeUP Group 2008-2013"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:34
+#: waeup/kofa/browser/templates/studentsitelayout.pt:38
+msgid "Contact"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:4
+#: waeup/kofa/browser/templates/studentsitelayout.pt:4
+msgid "WAeUP.Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:40
+#: waeup/kofa/browser/templates/studentsitelayout.pt:44
+msgid "Logout"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:62
+msgid "Student Id:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:66
+msgid "State:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/usereditformpage.pt:15
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:24
+#: waeup/kofa/students/browser_templates/change_password.pt:16
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:44
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:180
+msgid "Add"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:320
+msgid "Any link text"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:402 waeup/kofa/students/viewlets.py:160
+msgid "Send email"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:411
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:428 waeup/kofa/browser/viewlets.py:474
+#: waeup/kofa/browser/viewlets.py:504 waeup/kofa/browser/viewlets.py:534
+msgid "Export student data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:437
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:463 waeup/kofa/browser/viewlets.py:493
+#: waeup/kofa/browser/viewlets.py:523
+msgid "Show students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:558
+msgid "Show logs"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:567
+msgid "Upload data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:576
+msgid "Process data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:585
+msgid "Export data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:594
+msgid "View processed files"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:667
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:95 waeup/kofa/browser/viewlets.py:170
+#: waeup/kofa/students/viewlets.py:329 waeup/kofa/students/viewlets.py:536
+#: waeup/kofa/students/viewlets.py:583
+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:163
+#: waeup/kofa/hostels/browser.py:190
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:133 waeup/kofa/hostels/browser.py:168
+msgid "Clear all hostels"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:171
+msgid "All hostels cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:179
+msgid "Settings have been saved."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:193
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:202
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:204
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:239
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:242 waeup/kofa/hostels/browser.py:270
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:243 waeup/kofa/hostels/browser.py:284
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:244 waeup/kofa/hostels/browser.py:314
+msgid "Release selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:245 waeup/kofa/hostels/browser.py:341
+msgid "Clear hostel"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:275
+msgid ""
+"${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:308
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:332
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:337
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:344
+msgid "Hostel cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:357
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:363
+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/containerpage.pt:21
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:37
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:45
+msgid "Id"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:23
+msgid "Booked Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:47
+msgid "Number"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:6
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:66
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelpage.pt:5
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:19
+msgid "Reserved Beds:"
+msgstr ""
+
+#: waeup/kofa/hostels/export.py:61
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:9
+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:105
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:111
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:117
+msgid "Reserved Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:128
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:135
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:142
+msgid "Beds for Pre-Study Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:149
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:156
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:163
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:170
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:177
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:188
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:198
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:220
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:226
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:232
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:237
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:238
+msgid "Enter valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:253
+msgid ""
+"Either student does not exist or student is not in accommodation session."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/interfaces.py:260
+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:93
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:99
+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 K"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:63
+msgid "Block L"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:64
+msgid "Block M"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:65
+msgid "Block N"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:66
+msgid "Block O"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:67
+msgid "Block P"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:68
+msgid "Block Q"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:69
+msgid "Block R"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:70
+msgid "Block S"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:71
+msgid "Block T"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:72
+msgid "Block U"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:73
+msgid "Block V"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:74
+msgid "Block W"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:207
+msgid "Subject"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:211 waeup/kofa/students/browser.py:1134
+#: waeup/kofa/students/browser.py:1297
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:10
+msgid "Grade"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:368
+msgid "Processor name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:394
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:401
+msgid "Email to:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:408
+msgid "Subject:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:412
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:416
+msgid "Text:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:445 waeup/kofa/interfaces.py:501
+msgid "Public Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:455 waeup/kofa/interfaces.py:456
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:462
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:463
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:492
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:97
+#: waeup/kofa/browser/templates/userscontainerpage.pt:4
+#: waeup/kofa/browser/templates/facultymanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:93
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:108
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:87
+msgid "User Id"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:497
+#: waeup/kofa/students/browser_templates/containerpage.pt:36
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:42
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:48
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:29
+msgid "Full Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:507
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:524
+#: waeup/kofa/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:571
+msgid "Name of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:572
+msgid "Sample University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:577
+msgid "Abbreviated Title of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:583
+msgid "Skin"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:59
+msgid "new"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:590
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:60
+msgid "completed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:602
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:608
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:61
+msgid "pending"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:615
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:616
+msgid "Kofa Contact"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:62
+msgid "active"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:621
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:628
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:63
+msgid "assigned"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:635
+msgid "Carry-over Course Registration"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:64
+msgid "callbacks"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:644 waeup/kofa/interfaces.py:690
+msgid "Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:658
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:664
+msgid "Bed Booking Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:670
+msgid "Rent"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:676
+msgid "Clearance enabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:92
+msgid "clearance started"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:93
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:94
+msgid "cleared"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:95
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:96
+msgid "courses registered"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:97
+msgid "courses validated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:98
+msgid "returning"
+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. Proceed to the login page and enter your "
+"credentials."
+msgstr ""
+
+#: waeup/kofa/objecthistory.py:74
+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:782
+#: waeup/kofa/students/interfaces.py:806
+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:789
+#: waeup/kofa/students/interfaces.py:813
+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/applicants/browser_templates/applicanteditpage.pt:92
+#: 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:853
+msgid "System"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1024
+msgid "You must select a session for the level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1033
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1035 waeup/kofa/students/browser.py:2477
+msgid "This level exists."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1045
+msgid "No study level selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1073
+msgid "You are not allowed to view the transcript."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1088
+msgid "${a}: Transcript Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1106
+msgid "You are not allowed to download the transcript."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1121 waeup/kofa/students/viewlets.py:366
+msgid "Academic Transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1125
+msgid "CERTIFIED TRUE COPY"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1129 waeup/kofa/students/browser.py:1289
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:35
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:25
+msgid "Sem."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1132 waeup/kofa/students/interfaces.py:623
+#: waeup/kofa/university/interfaces.py:152
+#: waeup/kofa/students/browser_templates/transcript.pt:39
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:19
+msgid "Credits"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1133 waeup/kofa/students/browser.py:1296
+#: waeup/kofa/students/interfaces.py:634 waeup/kofa/students/interfaces.py:667
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:20
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:32
+msgid "Score"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1161 waeup/kofa/students/viewlets.py:203
+msgid "Transfer student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1171
+msgid "Transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1175
+msgid "Current level does not match certificate levels."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1177
+msgid "Former study course record incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1179
+msgid "Maximum number of transfers exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1181
+msgid "Successfully transferred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1191
+msgid "Revert previous transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1196
+msgid "No previous transfer."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1201
+msgid "Revert now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1204
+msgid "Previous transfer reverted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1235
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1256
+msgid "Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1262
+msgid "1st Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1268
+msgid "2nd Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1274
+msgid "Level Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1283
+msgid "Course Registration Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1292
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:38
+msgid "Dept."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1293
+#: waeup/kofa/browser/templates/searchpage.pt:15
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:22
+msgid "Faculty"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1294
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:40
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:30
+msgid "Cred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1342 waeup/kofa/students/browser.py:1481
+#: waeup/kofa/students/browser.py:1492 waeup/kofa/students/browser.py:2602
+msgid "Add course ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1360 waeup/kofa/students/browser.py:2507
+msgid "No valid course code entered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1366 waeup/kofa/students/browser.py:2512
+msgid "Course not found."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1383
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1397 waeup/kofa/students/browser.py:2536
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:141
+msgid "Clearance is disabled for this session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1427 waeup/kofa/students/browser.py:1452
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1431
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1433 waeup/kofa/students/browser.py:1464
+msgid "Student is in the wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1457
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1461
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1516
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:152
+msgid "Total credits exceed ${a}."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1541
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1574
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:159
+msgid "The ticket exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:161
+msgid "Successfully added ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1622
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1630 waeup/kofa/students/browser.py:1680
+#: waeup/kofa/students/browser.py:1711
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1640 waeup/kofa/students/browser.py:1931
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1646
+msgid "You have not yet booked accommodation."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1670
+msgid "Add previous session online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1675
+msgid "No previous payment to be made."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1708
+msgid "Add balance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:182 waeup/kofa/students/export.py:197
+#: waeup/kofa/students/reports.py:154 waeup/kofa/students/viewlets.py:688
+msgid "Students"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1844
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1856
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1863
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1895
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1897
+msgid "Create bed ticket"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1914
+msgid "Outside booking period: ${a} - ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1917
+msgid "Outside booking period."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1921
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1925
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1936
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1949 waeup/kofa/students/browser.py:2268
+#: waeup/kofa/students/browser.py:2410
+msgid "Activation code is invalid."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1961 waeup/kofa/students/browser.py:1976
+msgid "System error: Please contact the adminsitrator."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1981 waeup/kofa/students/browser.py:2117
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1987 waeup/kofa/students/browser.py:2271
+#: waeup/kofa/students/browser.py:2414
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1990 waeup/kofa/students/browser.py:2275
+#: waeup/kofa/students/browser.py:2417 waeup/kofa/students/browser.py:2662
+msgid "invalidated"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1995 waeup/kofa/students/browser.py:2279
+#: waeup/kofa/students/browser.py:2422
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2007 waeup/kofa/students/browser.py:2131
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2016
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2033
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2052
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:206 waeup/kofa/students/viewlets.py:119
+#: waeup/kofa/students/viewlets.py:751
+msgid "Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2061
+msgid "Bed Allocation Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:208
+msgid "Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2089
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2095
+msgid "Student can't be relocated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2139
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:214 waeup/kofa/students/viewlets.py:125
+#: waeup/kofa/students/viewlets.py:752
+msgid "Payments"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2158
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2170
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2185 waeup/kofa/students/viewlets.py:545
+msgid "Change password"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2199
+msgid "Password changed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2211
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2226 waeup/kofa/students/viewlets.py:569
+#: waeup/kofa/students/workflow.py:48
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2230
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2248
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2252
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2256
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2283
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:229 waeup/kofa/students/viewlets.py:131
+msgid "Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2293
+msgid "Edit clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2314 waeup/kofa/students/browser.py:2328
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2322
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2339 waeup/kofa/students/browser.py:2373
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2342 waeup/kofa/students/browser.py:2376
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2351 waeup/kofa/students/workflow.py:62
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2352
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2355
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2367
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:238
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2385
+msgid "Start session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2389
+msgid "Start now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2398
+msgid "You are not entitled to start session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2435
+msgid "An error occurred, please contact the system administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2437
+msgid "Session started."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2456
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2468
+msgid "Create course list now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2479
+msgid "Your data are incomplete"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2524
+msgid "Edit course list of ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2565
+msgid "You are a postgraduate student, your course list can't bee registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:257
+msgid "Student Section"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2573
+msgid "Maximum credits of ${a} exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2578
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2582
+msgid "Register course list"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2618
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2621
+msgid "Set"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2636
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2644
+msgid "Access code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2647
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2653
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2659
+msgid "Access code has already been used."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2668
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2680
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2723 waeup/kofa/students/browser.py:2734
+msgid "No student record found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2768
+msgid "Your password request was successful."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2784 waeup/kofa/students/interfaces.py:417
+#: waeup/kofa/students/interfaces.py:508
+#: waeup/kofa/browser/templates/showstudentspage.pt:8
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:60
+msgid "Current Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2785 waeup/kofa/students/interfaces.py:424
+#: waeup/kofa/students/interfaces.py:456 waeup/kofa/students/interfaces.py:514
+#: waeup/kofa/browser/templates/showstudentspage.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:61
+msgid "Current Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:282
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2836
+msgid "in any session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2842
+msgid "at any level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:287 waeup/kofa/students/browser.py:331
+#: waeup/kofa/students/browser.py:2633 waeup/kofa/students/browser.py:2845
+msgid "No student found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2878 waeup/kofa/students/interfaces.py:539
+#: waeup/kofa/students/interfaces.py:714 waeup/kofa/students/utils.py:224
+#: waeup/kofa/students/browser_templates/transcript.pt:35
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:8
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:14
+msgid "Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2879 waeup/kofa/students/utils.py:226
+#: waeup/kofa/university/interfaces.py:340
+#: waeup/kofa/browser/templates/certificatepage.pt:28
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:34
+#: waeup/kofa/students/browser_templates/transcript.pt:34
+msgid "Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2936
+msgid "Configure student data export"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2944
+msgid "All Sessions"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2951
+msgid "All Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2958
+msgid "All Modes"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:298 waeup/kofa/students/viewlets.py:144
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3022
+msgid "Export started for students with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3113
+msgid "Export started for course tickets with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:358 waeup/kofa/students/viewlets.py:151
+msgid "Add student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:366
+msgid "Student record created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:380
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:402
+msgid "Login now"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:406
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:412
+msgid "You successfully logged in as student."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:430
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:432
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:509
+msgid "Admission Letter of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:527
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:576
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:616
+msgid "Student account has been activated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:635
+msgid "Student account has been deactivated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:670
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:700 waeup/kofa/students/viewlets.py:109
+#: waeup/kofa/students/viewlets.py:749
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:6
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:706
+msgid "Clearance Slip of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:715
+msgid "Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:716
+msgid "Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:723
+msgid "Date, Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:724
+msgid "Date, Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:745
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:789
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:791 waeup/kofa/students/browser.py:826
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:803 waeup/kofa/students/viewlets.py:277
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:815
+msgid "Save comment and reject clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:819
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:823
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:856
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:869
+msgid "Manage personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:884
+msgid "Edit personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:887
+msgid "Save/Confirm"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:914
+msgid "${a}: Study Course"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:917
+msgid "${a}: Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:945
+msgid "First Study Course, "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:947
+msgid "Second Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:952
+msgid "First Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:963
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:966 waeup/kofa/students/browser.py:1039
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:967 waeup/kofa/students/browser.py:1019
+msgid "Add study level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:999
+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/clearanceeditpage.pt:8
+msgid "Scans"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:31
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:38
+msgid "Select all"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:10
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:10
+msgid "with registration number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:11
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:11
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:12
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:12
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:78
+msgid "in session"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:13
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:13
+msgid "in department"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:14
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:14
+msgid "in study mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:15
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:15
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:33
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:27
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:34
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:40
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:35
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:41
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:15
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:25
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "State"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:5
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:5
+msgid "for students"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:8
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:8
+msgid "with id"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:9
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:9
+msgid "with name"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:18
+msgid "Weight"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:20
+msgid "The activation code can be found on the payment slip."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:34
+msgid ""
+"Your data have been approved. You can continue by clicking the button below."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:48
+msgid "Current Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig.pt:1
+msgid ""
+"<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."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:12
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:2
+#: waeup/kofa/students/browser_templates/exportconfig.pt:12
+msgid "Exporter (Data Type):"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:24
+#: waeup/kofa/students/browser_templates/exportconfig.pt:24
+msgid "Current Session:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:36
+#: waeup/kofa/students/browser_templates/exportconfig.pt:36
+msgid "Current Level:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:14
+msgid "Level Session:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:50
+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:93
+#: 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:94
+#: 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:90
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:46
+msgid "Payment Id"
+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: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:32
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:33
+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/studentsreportgeneratorpage.pt:10
+msgid "Study Modes Group:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.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/browser_templates/studentsreportgeneratorpage.pt:21
+msgid "Set study modes filter."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.pt:33
+msgid "<option value=\"${DYNAMIC_CONTENT}\"> ${DYNAMIC_CONTENT} </option>"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.pt:37
+msgid "Select academic session to report."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:6
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:9
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:50
+msgid "Previous Study Courses:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:64
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:30
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:69
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:35
+msgid "Level Code"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:70
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:36
+msgid "Level Title"
+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: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:39
+msgid "Fact."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:31
+msgid "Requ."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:43
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:21
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:34
+msgid "CO"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:44
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:35
+msgid "Auto"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:6
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:87
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:62
+msgid "Enter valid course code."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:93
+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:28
+msgid "Dep."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:29
+msgid "Fac."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:58
+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/export.py:220
+msgid "Student Study Courses"
+msgstr ""
+
+#: waeup/kofa/students/export.py:250
+msgid "Student Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/export.py:275
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:9
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:30
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:20
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:309
+msgid "Student Payments"
+msgstr ""
+
+#: waeup/kofa/students/export.py:346
+msgid "Payment Data for Bursary"
+msgstr ""
+
+#: waeup/kofa/students/export.py:377
+msgid "Bed Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:413
+msgid "Student Payments Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:442
+msgid "Student Study Levels Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:471
+msgid "Combo Card Data"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:191
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:193
+msgid ""
+"This message will be shown if and only if deactivated students try to login."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:232 waeup/kofa/students/interfaces.py:365
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:239
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:245
+msgid "Email"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:280
+msgid "Officer's Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:285
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:291
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:304
+msgid "Nationality"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:313
+msgid "Employer"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:323
+msgid "Updated"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:329 waeup/kofa/students/interfaces.py:347
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:376
+msgid "Registr. or Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:396 waeup/kofa/students/interfaces.py:450
+#: waeup/kofa/browser/templates/showstudentspage.pt:7
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:59
+msgid "Certificate"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:402 waeup/kofa/students/interfaces.py:482
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:409 waeup/kofa/students/interfaces.py:463
+#: waeup/kofa/students/interfaces.py:489
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:431 waeup/kofa/students/interfaces.py:502
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:438
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:520
+msgid "Bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:525 waeup/kofa/students/interfaces.py:552
+msgid "Validated by"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:545
+msgid "Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:558
+msgid "Validation Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:564
+msgid "Total Credits"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:602
+msgid "Faculty Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:607
+msgid "Department Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:612
+#: waeup/kofa/university/interfaces.py:164
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:618
+#: waeup/kofa/university/interfaces.py:158
+msgid "Passmark"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:628
+#: waeup/kofa/browser/templates/certificatepage.pt:33
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:39
+msgid "Required"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:640
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:646
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:657
+#: waeup/kofa/university/interfaces.py:334
+#: waeup/kofa/university/interfaces.py:369
+#: waeup/kofa/students/browser_templates/transcript.pt:37
+msgid "Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:673
+msgid "Level Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:702
+msgid "Allocated Bed"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:708
+msgid "Requested Bed Type"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:721
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:727
+msgid "Booking Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:748
+msgid "Current Session Payment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:754 waeup/kofa/students/interfaces.py:795
+#: waeup/kofa/students/interfaces.py:819
+msgid "Payment Level"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:825
+msgid "Balance Amount"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:829
+msgid "Balance in Naira "
+msgstr ""
+
+#: waeup/kofa/students/reports.py:176
+msgid "Create students report"
+msgstr ""
+
+#: waeup/kofa/students/reports.py:196
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:122
+msgid "Name:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:141
+msgid "Study Course:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:148 waeup/kofa/students/utils.py:734
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:157 waeup/kofa/students/utils.py:727
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:166 waeup/kofa/students/utils.py:741
+msgid "Entry Session:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:174
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:261
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:338
+msgid "The previous session must not fall below your entry session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:343 waeup/kofa/students/utils.py:346
+msgid "This is not a previous session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:363 waeup/kofa/students/utils.py:402
+msgid "Study course data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:417
+msgid "no bed allocated"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:419
+msgid "Amount could not be determined."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:452
+msgid "Amount must be greater than 0."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:516
+msgid ""
+"This is to inform you that you have been provisionally admitted into ${a} as "
+"follows:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:556
+msgid "Your Kofa student record was created on ${a}."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:608 waeup/kofa/students/viewlets.py:76
+#: waeup/kofa/students/viewlets.py:104
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:7
+msgid "Base Data"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:628
+msgid "Workflow History"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:721
+msgid "Course of Study:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:748
+msgid "Entry Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:755
+msgid "Cumulative GPA:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:786
+msgid "Error in image file."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:814
+msgid "(not yet)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:114 waeup/kofa/students/viewlets.py:750
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:136 waeup/kofa/students/viewlets.py:755
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:27
+msgid "History"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:177
+msgid "Trigger transition"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:186
+msgid "Login as student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:195
+msgid "Download admission letter"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:212
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:224 waeup/kofa/students/viewlets.py:244
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:232
+msgid "Activate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:260 waeup/kofa/students/workflow.py:76
+msgid "Clear student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:295
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:304 waeup/kofa/students/viewlets.py:313
+msgid "View"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:351
+msgid "Transcript"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:382
+msgid "Reactivate this study course (revert previous transfer)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:411 waeup/kofa/students/workflow.py:160
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:429
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:448
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:510
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:519
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:528
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:554
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:598
+msgid "Start new session"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:612
+msgid "Add course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:631
+msgid "Edit course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:649
+msgid "Add current session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:657
+msgid "Add previous session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:672
+msgid "Add balance payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:754
+msgid "Accommodation Data"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:766
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:780
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:786
+msgid "Could not determine file type."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:791
+msgid "${a} file extension expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:797
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:806
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:824
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:10
+#: waeup/kofa/browser/templates/datacenterpage.pt:17
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:11
+msgid "File"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:825
+msgid "Scan"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:848
+msgid "Upload new file"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:849
+msgid "Delete attachment"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:886
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:90
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:900
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:9
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:911
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:926 waeup/kofa/students/viewlets.py:937
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:927 waeup/kofa/students/viewlets.py:938
+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: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:10
+#: waeup/kofa/browser/templates/departmentpage.pt:3
+#: waeup/kofa/browser/templates/certificatepage.pt:22
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:28
+msgid "Courses"
+msgstr ""
+
+#: waeup/kofa/university/export.py:165
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:13
+#: 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:146
+msgid "Title of course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:171
+msgid "Former Course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:172
+msgid ""
+"If this attribute is being set all certificate courses referring to this "
+"course will be automatically deleted."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:189
+msgid "Enter unique course code which will become part of the URL."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:209
+msgid "Unnamed Certificate"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:214
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:221
+msgid "Start Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:228
+msgid "End Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:235
+msgid "Application Category"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:242
+msgid "Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:247
+msgid "Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:252
+msgid "Foreigner Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:257
+msgid "Foreigner Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:262
+msgid "Installment Ratio"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:269
+msgid "Custom Textline 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:274
+msgid "Custom Textline 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:279
+msgid "Custom Float 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:284
+msgid "Custom Float 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:296
+#: waeup/kofa/university/interfaces.py:298
+msgid "Start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:300
+#: waeup/kofa/university/interfaces.py:303
+msgid "Study mode, start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:313
+msgid "Enter unique certificate code which will become part of the URL."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:347
+msgid "Registration required"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:45
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:46
+#: waeup/kofa/university/interfaces.py:94
+#: waeup/kofa/university/interfaces.py:147
+msgid "Unnamed"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:51
+#: waeup/kofa/university/interfaces.py:99
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:67
+#: waeup/kofa/university/interfaces.py:118
+msgid "This code will become part of the URL."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:93
+msgid "Name of department"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:29
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:30
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:31
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:32
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:33
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:34
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:35
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:36
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:37
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:38
+msgid "900 (Year 9)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:39
+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:186
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:239
+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:18
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/kofa/widgets/sequencewidget.pt:21
+msgid "remove-selected-items"
+msgstr ""
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/waeup.kofa.pot
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/waeup.kofa.pot	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/waeup.kofa.pot	(revision 10886)
@@ -0,0 +1,6623 @@
+##############################################################################
+#
+# 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: Development/Unknown\n"
+"POT-Creation-Date: Tue Dec 10 07:18:56 2013\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"
+"Generated-By: zope/app/locales/extract.py\n"
+
+#: waeup/kofa/accesscodes/browser.py:102
+#: waeup/kofa/accesscodes/browser.py:115
+#: waeup/kofa/applicants/browser.py:263
+#: waeup/kofa/applicants/browser.py:360
+#: waeup/kofa/applicants/browser.py:361
+#: waeup/kofa/applicants/browser.py:481
+#: waeup/kofa/browser/pages.py:654
+#: waeup/kofa/browser/pages.py:864
+#: waeup/kofa/browser/pages.py:890
+#: waeup/kofa/browser/pages.py:957
+#: waeup/kofa/browser/pages.py:1183
+#: waeup/kofa/browser/pages.py:1213
+#: waeup/kofa/browser/pages.py:1316
+#: waeup/kofa/browser/pages.py:1547
+#: waeup/kofa/browser/pages.py:1713
+#: waeup/kofa/browser/pages.py:1737
+#: waeup/kofa/browser/pages.py:1768
+#: waeup/kofa/browser/pages.py:1797
+#: waeup/kofa/browser/pages.py:1798
+#: waeup/kofa/browser/pages.py:1831
+#: waeup/kofa/browser/pages.py:1926
+#: waeup/kofa/browser/pages.py:1972
+#: waeup/kofa/browser/pages.py:1973
+#: waeup/kofa/browser/pages.py:1975
+#: waeup/kofa/browser/pages.py:2040
+#: waeup/kofa/browser/pages.py:2089
+#: waeup/kofa/browser/pages.py:2130
+#: waeup/kofa/browser/pages.py:2161
+#: waeup/kofa/browser/pages.py:2182
+#: waeup/kofa/browser/pages.py:2230
+#: waeup/kofa/browser/pages.py:2232
+#: waeup/kofa/browser/pages.py:2265
+#: waeup/kofa/browser/pages.py:2306
+#: waeup/kofa/browser/pages.py:2363
+#: waeup/kofa/students/browser.py:971
+#: waeup/kofa/students/browser.py:972
+#: waeup/kofa/students/browser.py:1485
+#: waeup/kofa/students/browser.py:1487
+#: waeup/kofa/students/browser.py:1644
+#: waeup/kofa/students/browser.py:1803
+#: waeup/kofa/students/browser.py:1841
+#: waeup/kofa/students/browser.py:1874
+msgid "Cancel"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:104
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:114
+msgid "Reimport"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:117
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:122
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:128
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:138
+#. Default: ""
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:141
+#. Default: ""
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:154
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:155
+#: waeup/kofa/browser/pages.py:743
+#: waeup/kofa/browser/pages.py:1507
+#: waeup/kofa/browser/viewlets.py:202
+msgid "Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:156
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:157
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:182
+#: waeup/kofa/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:184
+#. Default: ""
+msgid "${a} disabled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:188
+#. Default: ""
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:192
+msgid "re-enabled"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:194
+#. Default: ""
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:198
+#. Default: ""
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:237
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:246
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:254
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:263
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:44
+msgid "Archive"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:45
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:47
+#: waeup/kofa/accesscodes/browser.py:208
+#: waeup/kofa/accesscodes/browser.py:228
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:54
+msgid "No batch selected."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:62
+#. Default: ""
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:70
+#. Default: ""
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:79
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:85
+msgid "Create batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:93
+#. Default: ""
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:95
+#. 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:23
+msgid "Serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:24
+msgid "AC"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:26
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:48
+msgid "Owner"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:5
+msgid "for access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:7
+msgid "with PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:8
+msgid "with serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:9
+msgid "with message term"
+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:123
+msgid "Applicant has not yet been admitted."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:129
+msgid "Registration Number exists."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:132
+msgid "No course admitted provided."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:164
+#. Default: ""
+msgid "Student ${a} created"
+msgstr ""
+
+#: waeup/kofa/applicants/batching.py:211
+msgid "Application record imported"
+msgstr ""
+
+#: waeup/kofa/applicants/batching.py:258
+#: waeup/kofa/students/batching.py:216
+#. Default: ""
+msgid "State '${a}' set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1000
+#: waeup/kofa/students/browser.py:109
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1016
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1018
+msgid "Passport picture confirmation box not ticked."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1041
+msgid "1st and 2nd choice must be different."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1059
+msgid "The form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1065
+msgid "Form has been submitted."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1108
+#. Default: ""
+msgid "Apply for ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1113
+msgid "Outside application period."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1128
+#: waeup/kofa/browser/pages.py:2412
+#: waeup/kofa/students/browser.py:2845
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:113
+#: waeup/kofa/applicants/viewlets.py:90
+msgid "Find applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:114
+msgid "Find applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1151
+#: waeup/kofa/students/browser.py:2862
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1157
+#: waeup/kofa/applicants/browser.py:1169
+msgid "No application record found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1161
+#: waeup/kofa/students/browser.py:2871
+msgid "Your password has already been set and used. Please proceed to the login page."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1180
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1197
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1227
+#: waeup/kofa/browser/pages.py:1609
+#: waeup/kofa/students/browser.py:3142
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1250
+msgid "Export started."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:129
+#: waeup/kofa/browser/pages.py:1891
+#: waeup/kofa/students/browser.py:282
+#: waeup/kofa/students/browser.py:325
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:134
+msgid "No applicant found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:142
+#: waeup/kofa/applicants/viewlets.py:99
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:145
+#: waeup/kofa/applicants/browser.py:220
+#: waeup/kofa/applicants/browser.py:360
+#: waeup/kofa/applicants/browser.py:413
+#: waeup/kofa/applicants/browser.py:812
+#: waeup/kofa/applicants/browser.py:871
+#: waeup/kofa/applicants/browser.py:988
+#: waeup/kofa/applicants/browser.py:991
+#: waeup/kofa/applicants/browser.py:1035
+#: waeup/kofa/browser/pages.py:625
+#: waeup/kofa/browser/pages.py:782
+#: waeup/kofa/browser/pages.py:805
+#: waeup/kofa/browser/pages.py:884
+#: waeup/kofa/browser/pages.py:1545
+#: waeup/kofa/browser/pages.py:1797
+#: waeup/kofa/browser/pages.py:1827
+#: waeup/kofa/browser/pages.py:1972
+#: waeup/kofa/browser/pages.py:2011
+#: waeup/kofa/browser/pages.py:2161
+#: waeup/kofa/browser/pages.py:2178
+#: waeup/kofa/browser/pages.py:2230
+#: waeup/kofa/browser/pages.py:2251
+#: waeup/kofa/hostels/browser.py:132
+#: waeup/kofa/hostels/browser.py:177
+#: waeup/kofa/hostels/browser.py:242
+#: waeup/kofa/hostels/browser.py:266
+#: waeup/kofa/hostels/browser.py:361
+#: waeup/kofa/students/browser.py:548
+#: waeup/kofa/students/browser.py:601
+#: waeup/kofa/students/browser.py:776
+#: waeup/kofa/students/browser.py:878
+#: waeup/kofa/students/browser.py:971
+#: waeup/kofa/students/browser.py:999
+#: waeup/kofa/students/browser.py:1208
+#: waeup/kofa/students/browser.py:1485
+#: waeup/kofa/students/browser.py:1530
+#: waeup/kofa/students/browser.py:2317
+#: waeup/kofa/students/browser.py:2332
+#: waeup/kofa/students/browser.py:2456
+msgid "Save"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:146
+#: waeup/kofa/applicants/browser.py:176
+#: waeup/kofa/applicants/browser.py:361
+#: waeup/kofa/applicants/browser.py:432
+#: waeup/kofa/browser/pages.py:785
+#: waeup/kofa/browser/pages.py:827
+#: waeup/kofa/browser/pages.py:911
+#: waeup/kofa/browser/pages.py:1713
+#: waeup/kofa/browser/pages.py:1724
+#: waeup/kofa/browser/pages.py:1798
+#: waeup/kofa/browser/pages.py:1819
+#: waeup/kofa/hostels/browser.py:135
+#: waeup/kofa/hostels/browser.py:150
+#: waeup/kofa/students/browser.py:306
+#: waeup/kofa/students/browser.py:1984
+#: waeup/kofa/students/browser.py:1997
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:146
+#: waeup/kofa/applicants/browser.py:201
+#: waeup/kofa/applicants/browser.py:232
+#: waeup/kofa/applicants/browser.py:242
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:147
+#: waeup/kofa/applicants/browser.py:210
+#: waeup/kofa/applicants/browser.py:363
+#: waeup/kofa/applicants/browser.py:490
+#: waeup/kofa/browser/pages.py:1799
+#: waeup/kofa/browser/pages.py:1845
+#: waeup/kofa/browser/pages.py:1976
+#: waeup/kofa/browser/pages.py:2049
+#: waeup/kofa/browser/pages.py:2162
+#: waeup/kofa/browser/pages.py:2191
+#: waeup/kofa/browser/pages.py:2233
+#: waeup/kofa/browser/pages.py:2274
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:148
+#: waeup/kofa/applicants/browser.py:206
+#: waeup/kofa/applicants/browser.py:364
+#: waeup/kofa/applicants/browser.py:486
+#: waeup/kofa/browser/pages.py:1800
+#: waeup/kofa/browser/pages.py:1841
+#: waeup/kofa/browser/pages.py:1977
+#: waeup/kofa/browser/pages.py:2045
+#: waeup/kofa/browser/pages.py:2163
+#: waeup/kofa/browser/pages.py:2187
+#: waeup/kofa/browser/pages.py:2234
+#: waeup/kofa/browser/pages.py:2270
+msgid "Add local role"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:149
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:182
+msgid "No container selected!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:193
+#: waeup/kofa/applicants/browser.py:449
+#: waeup/kofa/applicants/browser.py:926
+msgid "Could not delete:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:196
+#: waeup/kofa/applicants/browser.py:452
+#: waeup/kofa/applicants/browser.py:929
+#: waeup/kofa/browser/pages.py:156
+#: waeup/kofa/students/browser.py:352
+#: waeup/kofa/students/browser.py:1061
+#: waeup/kofa/students/browser.py:1551
+#: waeup/kofa/students/browser.py:1746
+#: waeup/kofa/students/browser.py:2017
+#: waeup/kofa/students/browser.py:2694
+#. Default: ""
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:224
+#: waeup/kofa/applicants/browser.py:427
+#: waeup/kofa/applicants/browser.py:899
+#: waeup/kofa/applicants/browser.py:1044
+#: waeup/kofa/browser/pages.py:198
+#: waeup/kofa/browser/pages.py:2354
+#: waeup/kofa/hostels/browser.py:55
+#: waeup/kofa/students/browser.py:101
+#: waeup/kofa/students/browser.py:570
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:251
+msgid "An applicants container for the same application type and entrance year exists already in the database."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:259
+msgid "Added:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:271
+#: 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:283
+#: waeup/kofa/applicants/browser.py:1214
+msgid "Applicant Data Exports"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:362
+#: waeup/kofa/applicants/browser.py:457
+msgid "Create students from selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:370
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:438
+#: waeup/kofa/applicants/browser.py:463
+msgid "No applicant selected!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:474
+#: waeup/kofa/applicants/browser.py:633
+#. Default: ""
+msgid "${a} students successfully created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:477
+#: waeup/kofa/applicants/browser.py:636
+msgid "No student could be created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:504
+#: waeup/kofa/applicants/viewlets.py:115
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:507
+msgid "Create application record"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:512
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:524
+msgid "Applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:560
+#: waeup/kofa/students/browser.py:444
+msgid "set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:561
+#: waeup/kofa/students/browser.py:445
+msgid "unset"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:566
+#. Default: ""
+msgid "${a} <br /> Application Record ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:656
+#: waeup/kofa/students/utils.py:443
+#: waeup/kofa/students/utils.py:476
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:669
+#: waeup/kofa/students/browser.py:1799
+#: waeup/kofa/students/browser.py:1837
+#: waeup/kofa/students/browser.py:1870
+msgid "Payment ticket created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:692
+#: waeup/kofa/students/browser.py:1893
+#. Default: ""
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:70
+msgid "'You can not edit your application records after final submission. You really want to submit?'"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:737
+#: waeup/kofa/students/browser.py:1950
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:743
+#: waeup/kofa/students/browser.py:1956
+msgid "Online Payment Slip"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:769
+msgid "Please pay and submit before trying to download the application slip."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:790
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:795
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:812
+#: waeup/kofa/applicants/browser.py:991
+#: waeup/kofa/applicants/browser.py:1047
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:813
+#: waeup/kofa/applicants/browser.py:938
+#: waeup/kofa/applicants/browser.py:989
+#: waeup/kofa/applicants/browser.py:1024
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:813
+#: waeup/kofa/applicants/browser.py:943
+#: waeup/kofa/applicants/browser.py:989
+#: waeup/kofa/applicants/browser.py:992
+#: waeup/kofa/applicants/browser.py:1029
+#: waeup/kofa/students/browser.py:1487
+#: waeup/kofa/students/browser.py:1535
+#: waeup/kofa/students/browser.py:1727
+#: waeup/kofa/students/browser.py:2702
+msgid "Remove selected tickets"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:856
+#. Default: ""
+msgid "${a} <br /> Application Form ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:86
+msgid "Application Section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:868
+#: waeup/kofa/students/browser.py:598
+msgid "No transition"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:913
+#: waeup/kofa/students/browser.py:1733
+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:52
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:27
+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:25
+msgid "Passport Photo:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:50
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:66
+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:82
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:40
+msgid "Application Fee Payment Tickets"
+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/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:46
+msgid "Application Number"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:50
+msgid "First Choice"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:92
+#. 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."
+msgid "note_acmp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:93
+msgid "search form"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:38
+#. 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:56
+msgid "Records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:6
+msgid "Introduction"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:12
+msgid "Application Period"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:50
+msgid "Year"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:10
+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:1284
+#: waeup/kofa/students/browser.py:1439
+#: 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/browser/templates/departmentmanagepage.pt:39
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:68
+#: waeup/kofa/browser/templates/facultypage.pt:8
+#: waeup/kofa/browser/templates/departmentpage.pt:15
+#: waeup/kofa/browser/templates/departmentpage.pt:38
+#: waeup/kofa/browser/templates/searchpage.pt:13
+#: waeup/kofa/browser/templates/facultymanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:15
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:26
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:52
+msgid "Code"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:199
+#: waeup/kofa/students/browser.py:1285
+#: waeup/kofa/students/browser.py:1440
+#: waeup/kofa/students/interfaces.py:609
+#: waeup/kofa/university/interfaces.py:161
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:70
+#: waeup/kofa/browser/templates/facultypage.pt:9
+#: waeup/kofa/browser/templates/departmentpage.pt:16
+#: waeup/kofa/browser/templates/departmentpage.pt:39
+#: waeup/kofa/browser/templates/searchpage.pt:14
+#: waeup/kofa/browser/templates/certificatepage.pt:32
+#: waeup/kofa/browser/templates/facultymanagepage.pt:43
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:38
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:37
+#: waeup/kofa/students/browser_templates/transcript.pt:38
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:16
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:27
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:54
+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:666
+msgid "Application Fee"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:276
+msgid "Hide container"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:354
+#: waeup/kofa/students/interfaces.py:185
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:360
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:25
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:365
+#: waeup/kofa/students/interfaces.py:225
+#: waeup/kofa/students/interfaces.py:367
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:376
+#: waeup/kofa/applicants/interfaces.py:538
+#: waeup/kofa/students/interfaces.py:204
+#: waeup/kofa/students/interfaces.py:392
+msgid "First Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:380
+#: waeup/kofa/students/interfaces.py:209
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:384
+#: waeup/kofa/students/interfaces.py:214
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:388
+#: waeup/kofa/students/interfaces.py:297
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:394
+#: waeup/kofa/students/interfaces.py:219
+msgid "Sex"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:399
+#: waeup/kofa/applicants/interfaces.py:481
+#: waeup/kofa/applicants/interfaces.py:543
+#: waeup/kofa/browser/interfaces.py:221
+#: waeup/kofa/interfaces.py:440
+#: waeup/kofa/interfaces.py:516
+#: waeup/kofa/students/interfaces.py:397
+msgid "Email Address"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:404
+#: waeup/kofa/interfaces.py:445
+#: waeup/kofa/interfaces.py:523
+#: waeup/kofa/students/interfaces.py:250
+msgid "Phone"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:409
+#: waeup/kofa/applicants/interfaces.py:486
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:414
+#: waeup/kofa/applicants/interfaces.py:491
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:426
+#: waeup/kofa/applicants/interfaces.py:502
+msgid "Notice"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:430
+#: waeup/kofa/students/interfaces.py:199
+#: waeup/kofa/browser/templates/findstudentspage.pt:25
+#: waeup/kofa/students/browser_templates/containerpage.pt:34
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:37
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:56
+msgid "Student Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:435
+#: waeup/kofa/applicants/interfaces.py:496
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:440
+msgid "Form locked"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:446
+#: waeup/kofa/applicants/interfaces.py:507
+msgid "Special Application"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:53
+#: waeup/kofa/applicants/workflow.py:74
+#: waeup/kofa/students/payments.py:122
+msgid "Payment approved"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:66
+#: waeup/kofa/students/payments.py:111
+msgid "Successful payment"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:73
+#: waeup/kofa/students/payments.py:129
+msgid "This ticket has already been paid."
+msgstr ""
+
+#: waeup/kofa/applicants/pdf.py:65
+msgid "Application Record"
+msgstr ""
+
+#: waeup/kofa/applicants/pdf.py:99
+#. 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/utils.py:41
+msgid "Desired Study Courses"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:42
+msgid "Process Data"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:57
+#: waeup/kofa/students/browser.py:143
+#: waeup/kofa/students/utils.py:366
+#: waeup/kofa/students/utils.py:393
+#: waeup/kofa/students/utils.py:404
+#: waeup/kofa/students/utils.py:467
+msgid "Session configuration object is not available."
+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:225
+#: waeup/kofa/applicants/workflow.py:108
+#: waeup/kofa/students/browser.py:367
+msgid "Create student record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:242
+#: waeup/kofa/students/viewlets.py:488
+msgid "Download payment slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:257
+#: 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:94
+msgid "admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:44
+msgid "not admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:45
+#: waeup/kofa/interfaces.py:93
+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:93
+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:683
+msgid "Academics"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:127
+#: waeup/kofa/browser/pages.py:478
+msgid "Administration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:134
+#: waeup/kofa/browser/viewlets.py:337
+#: waeup/kofa/browser/viewlets.py:734
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:152
+#: waeup/kofa/browser/pages.py:525
+#: waeup/kofa/browser/viewlets.py:346
+#: waeup/kofa/browser/viewlets.py:744
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:159
+#: waeup/kofa/browser/pages.py:903
+#: waeup/kofa/browser/pages.py:1505
+#: waeup/kofa/browser/viewlets.py:355
+#: waeup/kofa/browser/viewlets.py:753
+msgid "Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:166
+#: waeup/kofa/browser/reports.py:37
+#: waeup/kofa/browser/viewlets.py:364
+#: waeup/kofa/browser/viewlets.py:762
+msgid "Reports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:174
+#: waeup/kofa/students/browser.py:3129
+msgid "Student Data Exports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:94
+msgid "Home"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:212
+msgid "Unique Identifier"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:213
+msgid "User Name, Student or Applicant Id, Matriculation or Registration Number"
+msgstr ""
+
+#: waeup/kofa/browser/layout.py:97
+msgid "'Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1002
+msgid "Import Manager"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1004
+#. Default: ""
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1008
+#. 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:102
+msgid "No local role selected."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1027
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1029
+#: waeup/kofa/browser/pages.py:2444
+#: waeup/kofa/students/browser.py:497
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1037
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1051
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1059
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1069
+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:1181
+#: waeup/kofa/browser/pages.py:1211
+#: waeup/kofa/browser/pages.py:1314
+#: waeup/kofa/browser/pages.py:1427
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1195
+#: waeup/kofa/browser/pages.py:1279
+#: waeup/kofa/browser/pages.py:1403
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1214
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1215
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:122
+#. Default: ""
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1258
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1266
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1291
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1317
+#: waeup/kofa/browser/pages.py:1546
+msgid "Reset"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1318
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1319
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1320
+msgid "Perform import"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:135
+#: waeup/kofa/browser/pages.py:917
+#: waeup/kofa/hostels/browser.py:291
+#: waeup/kofa/hostels/browser.py:321
+#: waeup/kofa/students/browser.py:339
+msgid "No item selected."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1385
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1429
+msgid "Process next"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1479
+#. Default: ""
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1481
+#. Default: ""
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1489
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1491
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1492
+msgid "Show"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1508
+msgid "Back"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1509
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1517
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1532
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1535
+msgid "No search results found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1543
+msgid "Edit data center settings"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1563
+#. Default: ""
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1566
+msgid "Given storage path cannot be used."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1567
+#. Default: ""
+msgid "Error: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1570
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1582
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1584
+msgid "Create CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1701
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1713
+#: waeup/kofa/browser/pages.py:1732
+#: waeup/kofa/browser/pages.py:1749
+#: waeup/kofa/browser/pages.py:1753
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1714
+#: waeup/kofa/university/export.py:36
+msgid "Faculties"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1718
+#: waeup/kofa/browser/viewlets.py:421
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1727
+msgid "You are not allowed to remove entire faculties."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1760
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1785
+#: waeup/kofa/browser/pages.py:1796
+#: waeup/kofa/university/export.py:78
+msgid "Departments"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1798
+#: waeup/kofa/browser/pages.py:1836
+#: waeup/kofa/browser/pages.py:1905
+#: waeup/kofa/browser/pages.py:1909
+msgid "Add department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1806
+#: waeup/kofa/browser/viewlets.py:449
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1822
+msgid "You are not allowed to remove entire departments."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1856
+#: waeup/kofa/students/browser.py:261
+#: waeup/kofa/students/browser.py:305
+msgid "Find student(s)"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1861
+msgid "Find students in "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1896
+#: waeup/kofa/students/browser.py:293
+#: waeup/kofa/students/browser.py:337
+#: waeup/kofa/students/browser.py:2778
+#: waeup/kofa/students/browser.py:2990
+#: waeup/kofa/students/browser.py:3087
+msgid "No student found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1916
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1919
+#. Default: ""
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1941
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1973
+#: waeup/kofa/browser/pages.py:2015
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1973
+#: waeup/kofa/browser/pages.py:2030
+#: waeup/kofa/browser/pages.py:2059
+#: waeup/kofa/browser/pages.py:2063
+msgid "Add course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1974
+#: waeup/kofa/browser/pages.py:2021
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1974
+#: waeup/kofa/browser/pages.py:2035
+#: waeup/kofa/browser/pages.py:2100
+#: waeup/kofa/browser/pages.py:2104
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1983
+#: waeup/kofa/browser/viewlets.py:466
+msgid "Manage department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2024
+msgid "You are not allowed to remove certificates."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2075
+msgid "A course with same code already exists: "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2081
+#. Default: ""
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2116
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2122
+#. Default: ""
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2159
+#: waeup/kofa/browser/viewlets.py:498
+msgid "Edit course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2224
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2231
+#: waeup/kofa/browser/pages.py:2260
+#: waeup/kofa/browser/pages.py:2287
+#: waeup/kofa/browser/pages.py:2289
+msgid "Add certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2232
+#: waeup/kofa/browser/pages.py:2255
+msgid "Remove selected certificate courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2238
+#: waeup/kofa/browser/viewlets.py:542
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2294
+msgid "The chosen certificate course is already part of this certificate."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2298
+#. Default: ""
+msgid "certificate course ${a}_${b} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2339
+#: waeup/kofa/browser/viewlets.py:574
+msgid "Edit certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2342
+msgid "Save and return"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2375
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2423
+msgid "No record found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2437
+#: waeup/kofa/students/browser.py:2893
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2441
+#. Default: ""
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:264
+#: waeup/kofa/browser/viewlets.py:283
+#: waeup/kofa/browser/templates/staffsitelayout.pt:28
+#: waeup/kofa/browser/templates/studentsitelayout.pt:32
+msgid "Login"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:275
+msgid "You logged in."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:284
+msgid "Your personal data record is outdated. Please update."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:318
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:325
+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 ""
+
+#: waeup/kofa/browser/pages.py:383
+#. Default: ""
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:389
+#: waeup/kofa/browser/pages.py:670
+msgid "Send message now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:408
+#: waeup/kofa/browser/pages.py:441
+#: waeup/kofa/browser/pages.py:687
+#: waeup/kofa/students/browser.py:495
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:428
+msgid "Send now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:443
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:468
+msgid "<h1>Welcome to WAeUP.Kofa</h1>"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:526
+#: waeup/kofa/hostels/browser.py:229
+#: 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:527
+#: waeup/kofa/browser/viewlets.py:192
+msgid "Remove"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:535
+#. Default: ""
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:572
+#: waeup/kofa/browser/pages.py:574
+#: waeup/kofa/browser/viewlets.py:580
+msgid "Add user"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:596
+#. Default: ""
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:617
+#. Default: ""
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:651
+msgid "User settings have been saved."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:668
+#: waeup/kofa/students/browser.py:476
+#. Default: ""
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:697
+#: waeup/kofa/browser/viewlets.py:371
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:710
+#: waeup/kofa/browser/viewlets.py:387
+msgid "My Roles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:741
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:768
+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:832
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:784
+#: waeup/kofa/browser/pages.py:811
+#: waeup/kofa/browser/pages.py:844
+msgid "Add session configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:823
+#. Default: ""
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:835
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:848
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:859
+msgid "The session chosen already exists."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:881
+#. Default: ""
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:928
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:931
+#. Default: ""
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:942
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:953
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:956
+msgid "Upload"
+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/datacenterlogspage.pt:12
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:16
+msgid "Date"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:533
+#. Default: ""
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:803
+#. Default: ""
+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
+#: waeup/kofa/students/browser_templates/exportconfig_course.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:6
+msgid "Certificate Courses"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:29
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:35
+msgid "Semester"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:36
+msgid "Cert. Course"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:37
+msgid "Dep. Course"
+msgstr ""
+
+#: waeup/kofa/browser/templates/changepw.pt:19
+#. 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/changepw.pt:8
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr ""
+
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:8
+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:34
+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/datacenterexportpage.pt:44
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:4
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:4
+msgid "Export Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:45
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:5
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:5
+msgid "Exporter"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:46
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:6
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:6
+msgid "Filter"
+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: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:16
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:18
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:41
+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:24
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:13
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:37
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:15
+msgid "Header"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:38
+msgid "change to"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:39
+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:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:14
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:36
+msgid "Reload page"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:12
+msgid "Overwrite contents in new location?"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:3
+#: waeup/kofa/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:7
+msgid "Copy existing data to new storage?"
+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:18
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:14
+msgid "Datasets"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:19
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:11
+msgid "Upload Date"
+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:113
+msgid "Download"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:22
+msgid "Processor:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:35
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:59
+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:67
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:72
+msgid "Processor"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:73
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:74
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:75
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:76
+msgid "CSV Skeleton File"
+msgstr ""
+
+#: waeup/kofa/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:101
+#: waeup/kofa/browser/templates/facultymanagepage.pt:75
+#: waeup/kofa/browser/templates/coursemanagepage.pt:34
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:97
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:112
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:91
+msgid "Local Role"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:16
+#: waeup/kofa/browser/templates/userscontainerpage.pt:7
+#: waeup/kofa/browser/templates/facultymanagepage.pt:16
+#: waeup/kofa/browser/templates/coursemanagepage.pt:6
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:17
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:16
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:6
+#: waeup/kofa/browser/templates/facultymanagepage.pt:6
+#: waeup/kofa/browser/templates/coursemanagepage.pt:5
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:5
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:7
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:9
+msgid "Settings"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:99
+#: waeup/kofa/browser/templates/userscontainerpage.pt:5
+#: waeup/kofa/browser/templates/facultymanagepage.pt:73
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:31
+#: waeup/kofa/browser/templates/coursemanagepage.pt:32
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:110
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:89
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:22
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:39
+msgid "Name"
+msgstr ""
+
+#: waeup/kofa/browser/templates/facultypage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/kofa/browser/templates/fileupload.pt:26
+#: waeup/kofa/students/browser_templates/imageupload.pt:20
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:32
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/findstudentspage.pt:10
+#: waeup/kofa/students/browser_templates/containerpage.pt:10
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:10
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/findstudentspage.pt:26
+#: waeup/kofa/students/browser_templates/containerpage.pt:35
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:26
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/findstudentspage.pt:27
+#: waeup/kofa/students/browser_templates/containerpage.pt:36
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:39
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/findstudentspage.pt:28
+#: waeup/kofa/students/browser_templates/containerpage.pt:37
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:40
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:15
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:25
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "State"
+msgstr ""
+
+#: waeup/kofa/browser/templates/findstudentspage.pt:7
+#: waeup/kofa/students/browser_templates/containerpage.pt:7
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:7
+msgid "with id"
+msgstr ""
+
+#: waeup/kofa/browser/templates/findstudentspage.pt:8
+#: waeup/kofa/students/browser_templates/containerpage.pt:8
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:8
+msgid "with name"
+msgstr ""
+
+#: waeup/kofa/browser/templates/findstudentspage.pt:9
+#: waeup/kofa/students/browser_templates/containerpage.pt:9
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:9
+msgid "with registration number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:13
+#: waeup/kofa/browser/templates/usereditformpage.pt:9
+#: waeup/kofa/students/browser_templates/basepage.pt:14
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:18
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:27
+#: waeup/kofa/students/browser_templates/change_password.pt:10
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:31
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:27
+msgid "Password:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:24
+msgid "Notice: User names, Ids and passwords are case sensitive."
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:36
+#. 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:39
+#. 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:44
+#. 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:48
+#. Default: "Or simply forgot your student id, application 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:5
+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/reportscontainercreate.pt:22
+msgid "Type of report to create"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:41
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:76
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:36
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:36
+msgid "<img src=\"${DYNAMIC_CONTENT}\" /> Reload"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:51
+msgid "Create new report"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:6
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:47
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:7
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:15
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:7
+msgid "Creator"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:7
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:48
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:91
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:47
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:8
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:8
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:49
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:9
+#: waeup/kofa/students/browser_templates/editscorespage.pt:11
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:59
+#: waeup/kofa/applicants/browser_templates/exportjobsindex.pt:9
+msgid "Status"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:16
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:17
+msgid "Department"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:17
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:46
+msgid "Type"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:9
+#: waeup/kofa/browser/templates/findstudentspage.pt:17
+#: waeup/kofa/students/browser_templates/containerpage.pt:26
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:17
+msgid "Search Results"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:113
+#: waeup/kofa/browser/templates/studentsitelayout.pt:97
+msgid "Back to bottom"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:122
+#: waeup/kofa/browser/templates/studentsitelayout.pt:107
+msgid "Back to top"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:124
+#: waeup/kofa/browser/templates/studentsitelayout.pt:109
+msgid "Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:127
+#: waeup/kofa/browser/templates/studentsitelayout.pt:112
+msgid "Copyright &copy; WAeUP Group 2008-2013"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:34
+#: waeup/kofa/browser/templates/studentsitelayout.pt:38
+msgid "Contact"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:4
+#: waeup/kofa/browser/templates/studentsitelayout.pt:4
+msgid "WAeUP.Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:40
+#: waeup/kofa/browser/templates/studentsitelayout.pt:44
+msgid "Logout"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:62
+msgid "Student Id:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:66
+msgid "State:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/usereditformpage.pt:15
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:24
+#: waeup/kofa/students/browser_templates/change_password.pt:16
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:44
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:182
+msgid "Add"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:322
+msgid "Any link text"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:404
+#: waeup/kofa/students/viewlets.py:168
+msgid "Send email"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:413
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:431
+#: waeup/kofa/browser/viewlets.py:489
+#: waeup/kofa/browser/viewlets.py:521
+#: waeup/kofa/browser/viewlets.py:565
+msgid "Export student data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:440
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:457
+msgid "Find students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:477
+#: waeup/kofa/browser/viewlets.py:509
+#: waeup/kofa/browser/viewlets.py:553
+msgid "Show students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:533
+msgid "Update scores"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:589
+msgid "Show logs"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:598
+msgid "Upload data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:607
+msgid "Process data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:616
+msgid "Export data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:625
+msgid "View processed files"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:698
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:95
+#: waeup/kofa/browser/viewlets.py:172
+#: 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:107
+msgid "Accommodation Section"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:120
+#: waeup/kofa/hostels/browser.py:130
+msgid "Manage accommodation section"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:133
+#: waeup/kofa/hostels/browser.py:164
+#: waeup/kofa/hostels/browser.py:191
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:134
+#: waeup/kofa/hostels/browser.py:169
+msgid "Clear all hostels"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:172
+msgid "All hostels cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:180
+msgid "Settings have been saved."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:194
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:203
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:205
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:240
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:243
+#: waeup/kofa/hostels/browser.py:271
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:244
+#: waeup/kofa/hostels/browser.py:285
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:245
+#: waeup/kofa/hostels/browser.py:315
+msgid "Release selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:246
+#: waeup/kofa/hostels/browser.py:342
+msgid "Clear hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:276
+#. Default: ""
+msgid "${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:309
+#. Default: ""
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:333
+#. Default: ""
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:338
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:345
+msgid "Hostel cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:358
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:364
+msgid "No valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:70
+#: waeup/kofa/hostels/browser.py:80
+#: waeup/kofa/hostels/export.py:37
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:13
+msgid "Hostels"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:97
+#. Default: ""
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:21
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:37
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:45
+msgid "Id"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:23
+msgid "Booked Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:47
+msgid "Number"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:6
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:66
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelpage.pt:5
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:19
+msgid "Reserved Beds:"
+msgstr ""
+
+#: waeup/kofa/hostels/export.py:61
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:9
+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 ""
+
+#: waeup/kofa/hostels/interfaces.py:265
+#. Default: ""
+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 validated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:101
+msgid "returning"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:102
+msgid "graduated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:103
+msgid "transcript requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:212
+msgid "Subject"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:216
+#: waeup/kofa/students/browser.py:1288
+#: waeup/kofa/students/browser.py:1446
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:10
+msgid "Grade"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:373
+msgid "Processor name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:399
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:406
+msgid "Email to:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:413
+msgid "Subject:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:417
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:421
+msgid "Text:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:450
+#: waeup/kofa/interfaces.py:506
+msgid "Public Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:460
+#: waeup/kofa/interfaces.py:461
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:467
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:468
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:497
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:97
+#: waeup/kofa/browser/templates/userscontainerpage.pt:4
+#: waeup/kofa/browser/templates/facultymanagepage.pt:71
+#: waeup/kofa/browser/templates/coursemanagepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:93
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:108
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:87
+msgid "User Id"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:502
+#: waeup/kofa/browser/templates/findstudentspage.pt:29
+#: waeup/kofa/students/browser_templates/containerpage.pt:38
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:41
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:48
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:28
+msgid "Full Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:512
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:529
+#: waeup/kofa/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:576
+msgid "Name of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:577
+msgid "Sample University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:582
+msgid "Abbreviated Title of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:588
+msgid "Skin"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:595
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:607
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:613
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:62
+msgid "new"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:620
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:621
+msgid "Kofa Contact"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:626
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:63
+msgid "completed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:633
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:64
+msgid "pending"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:640
+msgid "Carry-over Course Registration"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:645
+msgid "Current Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:646
+msgid "Session for which score editing is allowed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:65
+msgid "active"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:658
+#: waeup/kofa/interfaces.py:710
+msgid "Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:66
+msgid "assigned"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:67
+msgid "callbacks"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:672
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:678
+msgid "Bed Booking Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:684
+msgid "Rent (fallback)"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:690
+msgid "Transcript Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:696
+msgid "Clearance enabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:95
+msgid "clearance started"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:96
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:97
+msgid "cleared"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:98
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:99
+msgid "courses registered"
+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. Proceed to the login page and enter your credentials."
+msgstr ""
+
+#: waeup/kofa/objecthistory.py:74
+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:795
+#: waeup/kofa/students/interfaces.py:819
+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:802
+#: waeup/kofa/students/interfaces.py:826
+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/applicants/browser_templates/applicanteditpage.pt:92
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:48
+msgid "Payment Date"
+msgstr ""
+
+#: waeup/kofa/payments/interfaces.py:94
+msgid "Amount Authorized"
+msgstr ""
+
+#: waeup/kofa/payments/payment.py:96
+#. Default: ""
+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:1005
+msgid "Current level not available for certificate."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1030
+msgid "You must select a session for the level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1039
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1041
+#: waeup/kofa/students/browser.py:2622
+msgid "This level exists."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1051
+msgid "No study level selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1075
+#: waeup/kofa/students/viewlets.py:697
+msgid "Request transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1079
+msgid "Submit"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1091
+#: waeup/kofa/students/browser.py:2393
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1108
+#: waeup/kofa/students/browser.py:2093
+#: waeup/kofa/students/browser.py:2413
+#: waeup/kofa/students/browser.py:2555
+msgid "Activation code is invalid."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1111
+#: waeup/kofa/students/browser.py:2131
+#: waeup/kofa/students/browser.py:2416
+#: waeup/kofa/students/browser.py:2559
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1115
+#: waeup/kofa/students/browser.py:2134
+#: waeup/kofa/students/browser.py:2420
+#: waeup/kofa/students/browser.py:2562
+#: waeup/kofa/students/browser.py:2807
+msgid "invalidated"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1119
+#: waeup/kofa/students/browser.py:2139
+#: waeup/kofa/students/browser.py:2424
+#: waeup/kofa/students/browser.py:2567
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1141
+msgid "Transcript processing has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1153
+msgid "Process transcript request"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1154
+msgid "Save comment and mark as processed"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1177
+msgid "Transcript request processed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1192
+#: waeup/kofa/students/workflow.py:204
+#: waeup/kofa/students/workflow.py:205
+msgid "Transcript processed"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1205
+#: waeup/kofa/students/viewlets.py:712
+msgid "Manage transcript request"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1224
+msgid "You are not allowed to view the transcript."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1239
+#. Default: ""
+msgid "${a}: Transcript Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1257
+msgid "You are not allowed to download the transcript."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1272
+#: waeup/kofa/students/viewlets.py:374
+msgid "Academic Transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1276
+msgid "CERTIFIED TRUE COPY"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1283
+#: waeup/kofa/students/browser.py:1438
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:35
+#: waeup/kofa/students/browser_templates/transcript.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:25
+msgid "Term"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1286
+#: waeup/kofa/students/interfaces.py:635
+#: 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:1287
+#: waeup/kofa/students/browser.py:1445
+#: waeup/kofa/students/interfaces.py:646
+#: waeup/kofa/students/interfaces.py:680
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:20
+#: waeup/kofa/students/browser_templates/editscorespage.pt:9
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:32
+msgid "Score"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1316
+#: waeup/kofa/students/viewlets.py:211
+msgid "Transfer student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1326
+msgid "Transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1330
+msgid "Current level does not match certificate levels."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1332
+msgid "Former study course record incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1334
+msgid "Maximum number of transfers exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1336
+msgid "Successfully transferred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1346
+msgid "Revert previous transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1351
+msgid "No previous transfer."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1356
+msgid "Revert now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1359
+msgid "Previous transfer reverted."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1390
+#. Default: ""
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1411
+msgid "Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1420
+msgid "Course Registration Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1428
+msgid "1st Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1430
+msgid "2nd Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1432
+msgid "Level Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1441
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:38
+msgid "Dept."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1442
+#: waeup/kofa/browser/templates/searchpage.pt:15
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:22
+msgid "Faculty"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1443
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:40
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:30
+msgid "Cred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:145
+msgid "Clearance is disabled for this session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1486
+#: waeup/kofa/students/browser.py:1625
+#: waeup/kofa/students/browser.py:1636
+#: waeup/kofa/students/browser.py:2747
+msgid "Add course ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1504
+#: waeup/kofa/students/browser.py:2652
+msgid "No valid course code entered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1510
+#: waeup/kofa/students/browser.py:2657
+msgid "Course not found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1527
+#. Default: ""
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1541
+#: waeup/kofa/students/browser.py:2681
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:155
+#. Default: ""
+msgid "Total credits exceed ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1571
+#: waeup/kofa/students/browser.py:1596
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1575
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1577
+#: waeup/kofa/students/browser.py:1608
+msgid "Student is in the wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1601
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1605
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:162
+msgid "The ticket exists."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:164
+#. Default: ""
+msgid "Successfully added ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1660
+#. Default: ""
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1685
+#. Default: ""
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1718
+#. Default: ""
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1766
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1774
+#: waeup/kofa/students/browser.py:1824
+#: waeup/kofa/students/browser.py:1855
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1784
+#: waeup/kofa/students/browser.py:2075
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1790
+msgid "You have not yet booked accommodation."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1814
+msgid "Add previous session online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1819
+msgid "No previous payment to be made."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:185
+#: waeup/kofa/students/export.py:197
+#: waeup/kofa/students/viewlets.py:732
+msgid "Students"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1852
+msgid "Add balance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1988
+#. Default: ""
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2000
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2007
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2039
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2041
+msgid "Create bed ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2058
+#. Default: ""
+msgid "Outside booking period: ${a} - ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2061
+msgid "Outside booking period."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2065
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2069
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2080
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:209
+#: waeup/kofa/students/viewlets.py:126
+#: waeup/kofa/students/viewlets.py:798
+msgid "Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2105
+#: waeup/kofa/students/browser.py:2120
+msgid "System error: Please contact the adminsitrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:211
+msgid "Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2125
+#: waeup/kofa/students/browser.py:2261
+#. Default: ""
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2151
+#: waeup/kofa/students/browser.py:2275
+#. Default: ""
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2160
+#. Default: ""
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:217
+#: waeup/kofa/students/viewlets.py:132
+#: waeup/kofa/students/viewlets.py:799
+msgid "Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2177
+#. Default: ""
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2196
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2205
+msgid "Bed Allocation Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2233
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2239
+msgid "Student can't be relocated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2283
+#. Default: ""
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2302
+#. Default: ""
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2314
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:232
+#: waeup/kofa/students/viewlets.py:139
+msgid "Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2329
+#: waeup/kofa/students/viewlets.py:553
+msgid "Change password"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2343
+msgid "Password changed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2355
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2371
+#: waeup/kofa/students/viewlets.py:578
+#: waeup/kofa/students/workflow.py:48
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2375
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2397
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2401
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:241
+#. Default: ""
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2428
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2438
+msgid "Edit clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2459
+#: waeup/kofa/students/browser.py:2473
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2467
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2484
+#: waeup/kofa/students/browser.py:2518
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2487
+#: waeup/kofa/students/browser.py:2521
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2496
+#: waeup/kofa/students/workflow.py:62
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2497
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2500
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2512
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2530
+msgid "Start session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2534
+msgid "Start now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2543
+msgid "You are not entitled to start session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2580
+msgid "An error occurred, please contact the system administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2582
+msgid "Session started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:260
+msgid "Student Section"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2601
+#. Default: ""
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2613
+msgid "Create course list now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2624
+msgid "Your data are incomplete"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2669
+#. Default: ""
+msgid "Edit course list of ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2710
+msgid "You are a postgraduate student, your course list can't bee registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2718
+#. Default: ""
+msgid "Maximum credits of ${a} exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2723
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2727
+msgid "Register course list"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2763
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2766
+msgid "Set"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2781
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2789
+msgid "Access code is invalid."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2792
+#. Default: ""
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2798
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2804
+msgid "Access code has already been used."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2813
+#. Default: ""
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2825
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2868
+#: waeup/kofa/students/browser.py:2879
+msgid "No student record found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:288
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2913
+msgid "Your password request was successful."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2929
+#: waeup/kofa/students/interfaces.py:428
+#: waeup/kofa/students/interfaces.py:519
+#: waeup/kofa/students/browser_templates/editscorespage.pt:13
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:61
+msgid "Current Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2930
+#: waeup/kofa/students/interfaces.py:435
+#: waeup/kofa/students/interfaces.py:467
+#: waeup/kofa/students/interfaces.py:525
+#: waeup/kofa/students/browser_templates/editscorespage.pt:14
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:62
+msgid "Current Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2981
+msgid "in any session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2987
+msgid "at any level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3023
+#: waeup/kofa/students/interfaces.py:551
+#: waeup/kofa/students/interfaces.py:727
+#: waeup/kofa/students/utils.py:234
+#: waeup/kofa/students/browser_templates/transcript.pt:35
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:8
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:14
+msgid "Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3024
+#: waeup/kofa/students/utils.py:236
+#: waeup/kofa/university/interfaces.py:274
+#: waeup/kofa/browser/templates/certificatepage.pt:28
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:34
+#: waeup/kofa/students/browser_templates/transcript.pt:34
+msgid "Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:304
+#: waeup/kofa/students/viewlets.py:152
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3075
+msgid "Score editing disabled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3079
+msgid "Current academic session not set."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3116
+msgid "Error: Score(s) of %s have not be updated. Only integers are allowed., "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3155
+msgid "Configure student data export"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3163
+msgid "All Sessions"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3170
+msgid "All Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3177
+msgid "All Modes"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3241
+msgid "Export started for students with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3332
+msgid "Export started for course tickets with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:364
+#: waeup/kofa/students/viewlets.py:159
+msgid "Add student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:372
+msgid "Student record created."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:386
+#. Default: ""
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:408
+msgid "Login now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:412
+#. Default: ""
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:418
+msgid "You successfully logged in as student."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:436
+#. Default: ""
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:438
+#. Default: ""
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:515
+msgid "Admission Letter of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:533
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:582
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:622
+msgid "Student account has been activated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:641
+msgid "Student account has been deactivated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:676
+#. Default: ""
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:706
+#: waeup/kofa/students/viewlets.py:115
+#: waeup/kofa/students/viewlets.py:796
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:6
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:712
+msgid "Clearance Slip of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:721
+msgid "Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:722
+msgid "Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:729
+msgid "Date, Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:730
+msgid "Date, Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:751
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:795
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:797
+#: waeup/kofa/students/browser.py:832
+#: waeup/kofa/students/browser.py:1166
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:809
+#: waeup/kofa/students/viewlets.py:285
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:821
+msgid "Save comment and reject clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:825
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:829
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:862
+#. Default: ""
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:875
+msgid "Manage personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:890
+msgid "Edit personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:893
+msgid "Save/Confirm"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:920
+#. Default: ""
+msgid "${a}: Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:923
+#. Default: ""
+msgid "${a}: Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:951
+msgid "First Study Course, "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:953
+msgid "Second Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:958
+msgid "First Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:969
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:972
+#: waeup/kofa/students/browser.py:1045
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:973
+#: waeup/kofa/students/browser.py:1025
+msgid "Add study level"
+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/clearanceeditpage.pt:8
+msgid "Scans"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:30
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:38
+msgid "Select all"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:11
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:11
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:78
+msgid "in session"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:12
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:12
+msgid "in department"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:13
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:13
+msgid "in study mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:14
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:14
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:17
+msgid "who requested transcript (search term omitted)"
+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/filterstudentspage.pt:58
+msgid "Fullname"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:15
+msgid "Course Level"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/editscorespage.pt:8
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:57
+msgid "Matric No."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:34
+msgid "Your data have been approved. You can continue by clicking the button below."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:49
+msgid "Current Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig.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_certificate.pt:13
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:2
+#: waeup/kofa/students/browser_templates/exportconfig.pt:13
+msgid "Exporter (Data Type):"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:25
+#: waeup/kofa/students/browser_templates/exportconfig.pt:25
+msgid "Current Session:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:37
+#: waeup/kofa/students/browser_templates/exportconfig.pt:37
+msgid "Current Level:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:14
+msgid "Level Session:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:50
+msgid "and"
+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:93
+#: 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:94
+#: 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:90
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:46
+msgid "Payment Id"
+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: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:32
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:33
+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:6
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:9
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:50
+msgid "Previous Study Courses:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:64
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:30
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:69
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:35
+msgid "Level Code"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:70
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:36
+msgid "Level Title"
+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: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:39
+msgid "Fact."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:31
+msgid "Requ."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:43
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:21
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:34
+msgid "CO"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:44
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:35
+msgid "Auto"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:6
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:87
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:62
+msgid "Enter valid course code."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:93
+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:28
+msgid "Dep."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:29
+msgid "Fac."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:58
+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:16
+msgid "Comment:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:5
+msgid "Process request"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptprocess.pt:7
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:7
+msgid "View correspondence"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:13
+#. Default: ""
+msgid "transcript_request"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:18
+msgid "Cover Letter:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:26
+msgid "Dispatch Address:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:34
+#: waeup/kofa/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:49
+#: waeup/kofa/students/browser_templates/enterpin.pt:20
+msgid "The activation code can be found on the payment slip."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/transcriptrequest.pt:5
+msgid "Send new request"
+msgstr ""
+
+#: waeup/kofa/students/export.py:220
+msgid "Student Study Courses"
+msgstr ""
+
+#: waeup/kofa/students/export.py:250
+msgid "Student Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/export.py:275
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:9
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:30
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:20
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:309
+msgid "Student Payments"
+msgstr ""
+
+#: waeup/kofa/students/export.py:346
+msgid "Payment Data for Bursary"
+msgstr ""
+
+#: waeup/kofa/students/export.py:377
+msgid "Bed Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:413
+msgid "Student Payments Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:442
+msgid "Student Study Levels Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:471
+msgid "Combo Card Data"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:191
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:193
+msgid "This message will be shown if and only if deactivated students try to login."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:232
+#: waeup/kofa/students/interfaces.py:376
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:239
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:245
+msgid "Email"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:280
+msgid "Officer's Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:285
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:291
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:304
+msgid "Nationality"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:313
+msgid "Employer"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:323
+msgid "Updated"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:329
+#: waeup/kofa/students/interfaces.py:358
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:339
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:63
+msgid "Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:387
+msgid "Registr. or Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:407
+#: waeup/kofa/students/interfaces.py:461
+#: waeup/kofa/students/browser_templates/editscorespage.pt:12
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:60
+msgid "Certificate"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:413
+#: waeup/kofa/students/interfaces.py:493
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:420
+#: waeup/kofa/students/interfaces.py:474
+#: waeup/kofa/students/interfaces.py:500
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:442
+#: waeup/kofa/students/interfaces.py:513
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:449
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:531
+msgid "Bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:536
+#: waeup/kofa/students/interfaces.py:564
+msgid "Validated by"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:557
+msgid "Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:570
+msgid "Validation Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:576
+msgid "Total Credits"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:582
+msgid "Unrectified GPA"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:614
+msgid "Faculty Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:619
+msgid "Department Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:624
+#: waeup/kofa/university/interfaces.py:135
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:630
+#: waeup/kofa/university/interfaces.py:129
+msgid "Passmark"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:640
+#: waeup/kofa/browser/templates/certificatepage.pt:33
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:39
+msgid "Required"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:653
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:659
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:670
+#: waeup/kofa/university/interfaces.py:269
+#: waeup/kofa/students/browser_templates/transcript.pt:37
+msgid "Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:686
+msgid "Level Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:715
+msgid "Allocated Bed"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:721
+msgid "Requested Bed Type"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:734
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:740
+msgid "Booking Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:761
+msgid "Current Session Payment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:767
+#: waeup/kofa/students/interfaces.py:808
+#: waeup/kofa/students/interfaces.py:832
+msgid "Payment Level"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:838
+msgid "Balance Amount"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:842
+msgid "Balance in Naira "
+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:21
+msgid "Set department filter."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:53
+msgid "Select level to report."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:10
+msgid "Study Modes Group:"
+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:21
+msgid "Set study modes filter."
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:33
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:33
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:49
+msgid "<option value=\"${DYNAMIC_CONTENT}\"> ${DYNAMIC_CONTENT} </option>"
+msgstr ""
+
+#: waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt:37
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:37
+msgid "Select academic session to report."
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:193
+msgid "Level Report"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:217
+msgid "Create level report"
+msgstr ""
+
+#: waeup/kofa/students/reports/level_report.py:247
+#: waeup/kofa/students/reports/student_statistics.py:197
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:183
+msgid "Session Results Presentation"
+msgstr ""
+
+#: waeup/kofa/students/reports/session_results_presentation.py:205
+msgid "Create session results presentation"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:155
+msgid "Student Statistics"
+msgstr ""
+
+#: waeup/kofa/students/reports/student_statistics.py:177
+msgid "Create student statistics report"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:123
+msgid "Name:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:142
+msgid "Study Course:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:149
+#: waeup/kofa/students/utils.py:760
+#: waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt:10
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:158
+#: waeup/kofa/students/utils.py:753
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:170
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:176
+#: waeup/kofa/students/utils.py:767
+msgid "Entry Session:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:184
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:271
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:348
+msgid "The previous session must not fall below your entry session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:353
+#: waeup/kofa/students/utils.py:356
+msgid "This is not a previous session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:373
+#: waeup/kofa/students/utils.py:412
+msgid "Study course data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:432
+msgid "no bed allocated"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:436
+msgid "Amount could not be determined."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:469
+msgid "Amount must be greater than 0."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:533
+#. Default: ""
+msgid "This is to inform you that you have been provisionally admitted into ${a} as follows:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:578
+#. Default: ""
+msgid "Your Kofa student record was created on ${a}."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:628
+#: waeup/kofa/students/viewlets.py:76
+#: waeup/kofa/students/viewlets.py:109
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:7
+msgid "Base Data"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:648
+msgid "Workflow History"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:747
+msgid "Course of Study:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:774
+msgid "Entry Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:781
+msgid "Cumulative GPA:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:812
+msgid "Error in image file."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:840
+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:27
+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 ""
+
+#: waeup/kofa/students/viewlets.py:813
+#. Default: ""
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:827
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:833
+msgid "Could not determine file type."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:838
+#. Default: ""
+msgid "${a} file extension expected."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:844
+#. Default: ""
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:853
+#. Default: ""
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:871
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:10
+#: waeup/kofa/browser/templates/datacenterpage.pt:17
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:11
+msgid "File"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:872
+msgid "Scan"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:895
+msgid "Upload new file"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:896
+msgid "Delete attachment"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:92
+#: waeup/kofa/students/viewlets.py:792
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:933
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:947
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:9
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:958
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:973
+#: waeup/kofa/students/viewlets.py:984
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:974
+#: waeup/kofa/students/viewlets.py:985
+msgid "Birth Certificate Scan"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:112
+#. Default: ""
+msgid "${title} 2nd spillover"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:115
+#. Default: ""
+msgid "${title} 3rd spillover"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:118
+#. Default: ""
+msgid "${title} 1st spillover"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:120
+#. Default: ""
+msgid "${title} on 1st probation"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:122
+#. Default: ""
+msgid "${title} on 2nd probation"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:141
+msgid "male"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:143
+msgid "female"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:93
+#. Default: ""
+msgid "Error: wrong level id ${value}"
+msgstr ""
+
+#: waeup/kofa/students/vocabularies.py:98
+#: waeup/kofa/students/vocabularies.py:101
+#. Default: ""
+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:10
+#: waeup/kofa/browser/templates/departmentpage.pt:3
+#: waeup/kofa/browser/templates/certificatepage.pt:22
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:28
+msgid "Courses"
+msgstr ""
+
+#: waeup/kofa/university/export.py:165
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:13
+#: 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:29
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:30
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:31
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:32
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:33
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:34
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:35
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:36
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:37
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:38
+msgid "900 (Year 9)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:39
+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\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: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:191
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:244
+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:18
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#: waeup/kofa/widgets/sequencewidget.pt:21
+#. Default: "Remove selected items"
+msgid "remove-selected-items"
+msgstr ""
+
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/yo/LC_MESSAGES/waeup.kofa.po
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/yo/LC_MESSAGES/waeup.kofa.po	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/yo/LC_MESSAGES/waeup.kofa.po	(revision 10886)
@@ -0,0 +1,6189 @@
+# #############################################################################
+#
+# 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: Thu Jun 27 09:36:46 2013\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:102 waeup/kofa/accesscodes/browser.py:115
+#: waeup/kofa/applicants/browser.py:262 waeup/kofa/applicants/browser.py:351
+#: waeup/kofa/applicants/browser.py:352 waeup/kofa/applicants/browser.py:472
+#: waeup/kofa/browser/pages.py:636 waeup/kofa/browser/pages.py:846
+#: waeup/kofa/browser/pages.py:872 waeup/kofa/browser/pages.py:939
+#: waeup/kofa/browser/pages.py:1171 waeup/kofa/browser/pages.py:1201
+#: waeup/kofa/browser/pages.py:1304 waeup/kofa/browser/pages.py:1535
+#: waeup/kofa/browser/pages.py:1701 waeup/kofa/browser/pages.py:1725
+#: waeup/kofa/browser/pages.py:1756 waeup/kofa/browser/pages.py:1784
+#: waeup/kofa/browser/pages.py:1785 waeup/kofa/browser/pages.py:1828
+#: waeup/kofa/browser/pages.py:1873 waeup/kofa/browser/pages.py:1918
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1921
+#: waeup/kofa/browser/pages.py:1996 waeup/kofa/browser/pages.py:2045
+#: waeup/kofa/browser/pages.py:2086 waeup/kofa/browser/pages.py:2122
+#: waeup/kofa/browser/pages.py:2161 waeup/kofa/browser/pages.py:2163
+#: waeup/kofa/browser/pages.py:2196 waeup/kofa/browser/pages.py:2247
+#: waeup/kofa/browser/pages.py:2304 waeup/kofa/students/browser.py:965
+#: waeup/kofa/students/browser.py:966 waeup/kofa/students/browser.py:1341
+#: waeup/kofa/students/browser.py:1343 waeup/kofa/students/browser.py:1500
+#: waeup/kofa/students/browser.py:1659 waeup/kofa/students/browser.py:1697
+#: waeup/kofa/students/browser.py:1730
+msgid "Cancel"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:104
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:114
+msgid "Reimport"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:117
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:122
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:128
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:138
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:141
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:154
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:155 waeup/kofa/applicants/browser.py:85
+#: waeup/kofa/applicants/browser.py:113 waeup/kofa/browser/pages.py:725
+#: waeup/kofa/browser/pages.py:1495 waeup/kofa/browser/viewlets.py:200
+#: waeup/kofa/students/browser.py:258 waeup/kofa/students/browser.py:299
+msgid "Search"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:156
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:157
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:182 waeup/kofa/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:184
+msgid "${a} disabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:188
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:192
+msgid "re-enabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:194
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:198
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:237
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:246
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:254
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:263
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:44
+msgid "Archive"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:45
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:47 waeup/kofa/accesscodes/browser.py:208
+#: waeup/kofa/accesscodes/browser.py:228
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:54
+msgid "No batch selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:62
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:70
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:79
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser.py:85
+msgid "Create batch"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:93
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/accesscodes/browser.py:95
+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:23
+msgid "Serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:24
+msgid "AC"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:26
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:48
+msgid "Owner"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:5
+msgid "for access codes"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:7
+msgid "with PIN"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:8
+msgid "with serial"
+msgstr ""
+
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:9
+msgid "with message term"
+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:119
+msgid "Applicant has not yet been admitted."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:125
+msgid "Registration Number exists."
+msgstr ""
+
+#: waeup/kofa/applicants/applicant.py:128
+msgid "No course admitted provided."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/applicant.py:160
+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:1002
+msgid "1st and 2nd choice must be different."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1020
+msgid "The form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1026
+msgid "Form has been submitted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:1069
+msgid "Apply for ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1074
+msgid "Outside application period."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1089 waeup/kofa/browser/pages.py:2353
+#: waeup/kofa/students/browser.py:2700
+msgid "Send login credentials to email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1112 waeup/kofa/students/browser.py:2717
+msgid "An error occurred."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1118 waeup/kofa/applicants/browser.py:1130
+msgid "No application record found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:112 waeup/kofa/applicants/viewlets.py:90
+msgid "Search applicants"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1122 waeup/kofa/students/browser.py:2726
+msgid ""
+"Your password has already been set and used. Please proceed to the login "
+"page."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1141
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:1158
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:128 waeup/kofa/students/browser.py:276
+#: waeup/kofa/students/browser.py:319
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:133
+msgid "No applicant found."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:141 waeup/kofa/applicants/viewlets.py:99
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:144 waeup/kofa/applicants/browser.py:219
+#: waeup/kofa/applicants/browser.py:351 waeup/kofa/applicants/browser.py:404
+#: waeup/kofa/applicants/browser.py:795 waeup/kofa/applicants/browser.py:841
+#: waeup/kofa/applicants/browser.py:949 waeup/kofa/applicants/browser.py:952
+#: waeup/kofa/applicants/browser.py:996 waeup/kofa/browser/pages.py:607
+#: waeup/kofa/browser/pages.py:764 waeup/kofa/browser/pages.py:787
+#: waeup/kofa/browser/pages.py:866 waeup/kofa/browser/pages.py:1533
+#: waeup/kofa/browser/pages.py:1784 waeup/kofa/browser/pages.py:1824
+#: waeup/kofa/browser/pages.py:1918 waeup/kofa/browser/pages.py:1967
+#: waeup/kofa/browser/pages.py:2118 waeup/kofa/browser/pages.py:2161
+#: waeup/kofa/browser/pages.py:2182 waeup/kofa/hostels/browser.py:131
+#: waeup/kofa/hostels/browser.py:176 waeup/kofa/hostels/browser.py:241
+#: waeup/kofa/hostels/browser.py:265 waeup/kofa/hostels/browser.py:360
+#: waeup/kofa/students/browser.py:542 waeup/kofa/students/browser.py:595
+#: waeup/kofa/students/browser.py:770 waeup/kofa/students/browser.py:872
+#: waeup/kofa/students/browser.py:965 waeup/kofa/students/browser.py:993
+#: waeup/kofa/students/browser.py:1341 waeup/kofa/students/browser.py:1386
+#: waeup/kofa/students/browser.py:2173 waeup/kofa/students/browser.py:2188
+#: waeup/kofa/students/browser.py:2311
+msgid "Save"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:145 waeup/kofa/applicants/browser.py:175
+#: waeup/kofa/applicants/browser.py:352 waeup/kofa/applicants/browser.py:423
+#: waeup/kofa/browser/pages.py:767 waeup/kofa/browser/pages.py:809
+#: waeup/kofa/browser/pages.py:893 waeup/kofa/browser/pages.py:1701
+#: waeup/kofa/browser/pages.py:1712 waeup/kofa/browser/pages.py:1785
+#: waeup/kofa/browser/pages.py:1816 waeup/kofa/hostels/browser.py:134
+#: waeup/kofa/hostels/browser.py:149 waeup/kofa/students/browser.py:300
+#: waeup/kofa/students/browser.py:1840 waeup/kofa/students/browser.py:1853
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:145 waeup/kofa/applicants/browser.py:200
+#: waeup/kofa/applicants/browser.py:231 waeup/kofa/applicants/browser.py:241
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:146 waeup/kofa/applicants/browser.py:209
+#: waeup/kofa/applicants/browser.py:354 waeup/kofa/applicants/browser.py:481
+#: waeup/kofa/browser/pages.py:1786 waeup/kofa/browser/pages.py:1842
+#: waeup/kofa/browser/pages.py:1922 waeup/kofa/browser/pages.py:2005
+#: waeup/kofa/browser/pages.py:2164 waeup/kofa/browser/pages.py:2215
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:147 waeup/kofa/applicants/browser.py:205
+#: waeup/kofa/applicants/browser.py:355 waeup/kofa/applicants/browser.py:477
+#: waeup/kofa/browser/pages.py:1787 waeup/kofa/browser/pages.py:1838
+#: waeup/kofa/browser/pages.py:1923 waeup/kofa/browser/pages.py:2001
+#: waeup/kofa/browser/pages.py:2165 waeup/kofa/browser/pages.py:2211
+msgid "Add local role"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:148
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:181
+msgid "No container selected!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:192 waeup/kofa/applicants/browser.py:440
+#: waeup/kofa/applicants/browser.py:896
+msgid "Could not delete:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:195 waeup/kofa/applicants/browser.py:443
+#: waeup/kofa/applicants/browser.py:899 waeup/kofa/browser/pages.py:153
+#: waeup/kofa/students/browser.py:346 waeup/kofa/students/browser.py:1055
+#: waeup/kofa/students/browser.py:1407 waeup/kofa/students/browser.py:1602
+#: waeup/kofa/students/browser.py:1873 waeup/kofa/students/browser.py:2549
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:223 waeup/kofa/applicants/browser.py:418
+#: waeup/kofa/applicants/browser.py:869 waeup/kofa/applicants/browser.py:1005
+#: waeup/kofa/browser/pages.py:195 waeup/kofa/browser/pages.py:2295
+#: waeup/kofa/hostels/browser.py:55 waeup/kofa/students/browser.py:100
+#: waeup/kofa/students/browser.py:564
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:250
+msgid ""
+"An applicants container for the same application type and entrance year "
+"exists already in the database."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:258
+msgid "Added:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:270 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:353 waeup/kofa/applicants/browser.py:448
+msgid "Create students from selected"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:361
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:429 waeup/kofa/applicants/browser.py:454
+msgid "No applicant selected!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:465 waeup/kofa/applicants/browser.py:614
+msgid "${a} students successfully created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:468 waeup/kofa/applicants/browser.py:617
+msgid "No student could be created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:495 waeup/kofa/applicants/viewlets.py:115
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:498
+msgid "Create application record"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:503
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:517
+msgid "Applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:541 waeup/kofa/students/browser.py:438
+msgid "set"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:542 waeup/kofa/students/browser.py:439
+msgid "unset"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:547
+msgid "${a} <br /> Application Record ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:637 waeup/kofa/students/utils.py:426
+#: waeup/kofa/students/utils.py:459
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:649 waeup/kofa/students/browser.py:1655
+#: waeup/kofa/students/browser.py:1693 waeup/kofa/students/browser.py:1726
+msgid "Payment ticket created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:672 waeup/kofa/students/browser.py:1749
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:68
+msgid ""
+"'You can not edit your application records after final submission. You "
+"really want to submit?'"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:717 waeup/kofa/students/browser.py:1806
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:723 waeup/kofa/students/browser.py:1812
+msgid "Online Payment Slip"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:749
+msgid "Please pay and submit before trying to download the application slip."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:770
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:775
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:795 waeup/kofa/applicants/browser.py:952
+#: waeup/kofa/applicants/browser.py:1008
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:796 waeup/kofa/applicants/browser.py:908
+#: waeup/kofa/applicants/browser.py:950 waeup/kofa/applicants/browser.py:985
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:796 waeup/kofa/applicants/browser.py:913
+#: waeup/kofa/applicants/browser.py:950 waeup/kofa/applicants/browser.py:953
+#: waeup/kofa/applicants/browser.py:990 waeup/kofa/students/browser.py:1343
+#: waeup/kofa/students/browser.py:1391 waeup/kofa/students/browser.py:1583
+#: waeup/kofa/students/browser.py:2557
+msgid "Remove selected tickets"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/applicants/browser.py:826
+msgid "${a} <br /> Application Form ${b}"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:838 waeup/kofa/students/browser.py:592
+msgid "No transition"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:84
+msgid "Application Section"
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:883 waeup/kofa/students/browser.py:1589
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:961 waeup/kofa/students/browser.py:108
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:977
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/kofa/applicants/browser.py:979
+msgid "Passport picture confirmation box not ticked."
+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:28
+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:25
+msgid "Passport Photo:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:50
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:66
+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:82
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:40
+msgid "Application Fee Payment Tickets"
+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/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:46
+msgid "Application Number"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:50
+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:92
+msgid "note_acmp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:93
+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:38
+msgid "note_armp"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:56
+msgid "Records"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:6
+msgid "Introduction"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:12
+msgid "Application Period"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:50
+msgid "Year"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:11
+msgid "with email address"
+msgstr ""
+
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:5
+msgid "for applicant"
+msgstr ""
+
+#: waeup/kofa/applicants/export.py:39
+msgid "Basic Applicants Containers"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:143
+msgid "Create Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:144
+msgid "Update Application Records"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:174
+#: waeup/kofa/applicants/interfaces.py:238
+msgid "Human readable description in HTML format"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:192 waeup/kofa/students/browser.py:1130
+#: waeup/kofa/students/browser.py:1290 waeup/kofa/university/interfaces.py:38
+#: waeup/kofa/university/interfaces.py:66
+#: waeup/kofa/university/interfaces.py:86
+#: waeup/kofa/university/interfaces.py:117
+#: waeup/kofa/university/interfaces.py:139
+#: waeup/kofa/university/interfaces.py:188
+#: waeup/kofa/university/interfaces.py:201
+#: waeup/kofa/university/interfaces.py:311
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:39
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:68
+#: waeup/kofa/browser/templates/facultypage.pt:8
+#: waeup/kofa/browser/templates/departmentpage.pt:15
+#: waeup/kofa/browser/templates/departmentpage.pt:38
+#: waeup/kofa/browser/templates/searchpage.pt:13
+#: waeup/kofa/browser/templates/facultymanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:36
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:15
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:26
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:52
+msgid "Code"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:198 waeup/kofa/students/browser.py:1131
+#: waeup/kofa/students/browser.py:1291 waeup/kofa/students/interfaces.py:597
+#: waeup/kofa/university/interfaces.py:208
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:41
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:70
+#: waeup/kofa/browser/templates/facultypage.pt:9
+#: waeup/kofa/browser/templates/departmentpage.pt:16
+#: waeup/kofa/browser/templates/departmentpage.pt:39
+#: waeup/kofa/browser/templates/searchpage.pt:14
+#: waeup/kofa/browser/templates/certificatepage.pt:32
+#: waeup/kofa/browser/templates/facultymanagepage.pt:43
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:38
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:37
+#: waeup/kofa/students/browser_templates/transcript.pt:38
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:16
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:27
+#: waeup/kofa/applicants/browser_templates/applicantsrootpage.pt:11
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:54
+msgid "Title"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:204
+#: waeup/kofa/applicants/interfaces.py:318
+msgid "Application Target"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:211
+#: waeup/kofa/applicants/interfaces.py:325
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:218
+msgid "Application Mode"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:232
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:251
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:253
+#: waeup/kofa/applicants/interfaces.py:259 waeup/kofa/hostels/interfaces.py:39
+#: waeup/kofa/hostels/interfaces.py:45
+msgid "Example:"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:257
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:263
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:269 waeup/kofa/interfaces.py:652
+msgid "Application Fee"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:275
+msgid "Hide container"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:353
+#: waeup/kofa/students/interfaces.py:185
+msgid "Account suspended"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:359
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:26
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:364
+#: waeup/kofa/students/interfaces.py:225 waeup/kofa/students/interfaces.py:356
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:375
+#: waeup/kofa/applicants/interfaces.py:526
+#: waeup/kofa/students/interfaces.py:204 waeup/kofa/students/interfaces.py:381
+msgid "First Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:379
+#: waeup/kofa/students/interfaces.py:209
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:383
+#: waeup/kofa/students/interfaces.py:214
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:387
+#: waeup/kofa/students/interfaces.py:297
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:393
+#: waeup/kofa/students/interfaces.py:219
+msgid "Sex"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:398
+#: waeup/kofa/applicants/interfaces.py:474
+#: waeup/kofa/applicants/interfaces.py:531
+#: waeup/kofa/browser/interfaces.py:221 waeup/kofa/interfaces.py:435
+#: waeup/kofa/interfaces.py:511 waeup/kofa/students/interfaces.py:386
+msgid "Email Address"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:403 waeup/kofa/interfaces.py:440
+#: waeup/kofa/interfaces.py:518 waeup/kofa/students/interfaces.py:250
+msgid "Phone"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:408
+#: waeup/kofa/applicants/interfaces.py:479
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:413
+#: waeup/kofa/applicants/interfaces.py:484
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:425
+#: waeup/kofa/applicants/interfaces.py:495
+msgid "Notice"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:429
+#: waeup/kofa/students/interfaces.py:199
+#: waeup/kofa/browser/templates/showstudentspage.pt:4
+#: waeup/kofa/students/browser_templates/containerpage.pt:32
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:38
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:56
+msgid "Student Id"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:434
+#: waeup/kofa/applicants/interfaces.py:489
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/kofa/applicants/interfaces.py:439
+msgid "Form locked"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:53 waeup/kofa/applicants/workflow.py:74
+#: waeup/kofa/students/payments.py:115
+msgid "Payment approved"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:66 waeup/kofa/students/payments.py:104
+msgid "Successful payment"
+msgstr ""
+
+#: waeup/kofa/applicants/payment.py:73 waeup/kofa/students/payments.py:122
+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:40
+msgid "Desired Study Courses"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:41
+msgid "Process Data"
+msgstr ""
+
+#: waeup/kofa/applicants/utils.py:56 waeup/kofa/students/browser.py:139
+#: waeup/kofa/students/utils.py:356 waeup/kofa/students/utils.py:383
+#: waeup/kofa/students/utils.py:394 waeup/kofa/students/utils.py:450
+msgid "Session configuration object is not available."
+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:146 waeup/kofa/applicants/viewlets.py:155
+msgid "Create students"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:163
+msgid "View application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:171
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:179
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:197
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:213 waeup/kofa/applicants/workflow.py:108
+#: waeup/kofa/students/browser.py:361
+msgid "Create student record"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:230 waeup/kofa/students/viewlets.py:480
+msgid "Download payment slip"
+msgstr ""
+
+#: waeup/kofa/applicants/viewlets.py:245 waeup/kofa/applicants/workflow.py:73
+#: waeup/kofa/students/viewlets.py:495 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:728
+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:91
+msgid "admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:44
+msgid "not admitted"
+msgstr ""
+
+#: waeup/kofa/applicants/workflow.py:45 waeup/kofa/interfaces.py:90
+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:93
+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:652
+msgid "Academics"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:127 waeup/kofa/browser/pages.py:460
+msgid "Administration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:134 waeup/kofa/browser/viewlets.py:335
+#: waeup/kofa/browser/viewlets.py:703
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:152 waeup/kofa/browser/pages.py:507
+#: waeup/kofa/browser/viewlets.py:344 waeup/kofa/browser/viewlets.py:713
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:159 waeup/kofa/browser/pages.py:885
+#: waeup/kofa/browser/pages.py:1493 waeup/kofa/browser/viewlets.py:353
+#: waeup/kofa/browser/viewlets.py:722
+msgid "Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:166 waeup/kofa/browser/reports.py:37
+#: waeup/kofa/browser/viewlets.py:362 waeup/kofa/browser/viewlets.py:731
+msgid "Reports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:174 waeup/kofa/students/browser.py:2910
+msgid "Student Data Exports"
+msgstr ""
+
+#: waeup/kofa/browser/breadcrumbs.py:94
+msgid "Home"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:212
+msgid "Unique Identifier"
+msgstr ""
+
+#: waeup/kofa/browser/interfaces.py:213
+msgid ""
+"User Name, Student or Applicant Id, Matriculation or Registration Number"
+msgstr ""
+
+#: waeup/kofa/browser/layout.py:97
+msgid "'Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1009
+msgid "All import managers have been notified by email."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1011 waeup/kofa/browser/pages.py:2385
+#: waeup/kofa/students/browser.py:491
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1019
+msgid "Maximum number of files in the data center exceeded."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1033
+msgid "Only csv files are allowed."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1041
+msgid "File with same name was uploaded earlier."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1052 waeup/kofa/browser/pages.py:1059
+msgid "Your file contains forbidden characters. Please replace."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1169 waeup/kofa/browser/pages.py:1199
+#: waeup/kofa/browser/pages.py:1302 waeup/kofa/browser/pages.py:1415
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1183 waeup/kofa/browser/pages.py:1267
+#: waeup/kofa/browser/pages.py:1391
+msgid "Import aborted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:119
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1202
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1203
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1246
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1254
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1279
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1305 waeup/kofa/browser/pages.py:1534
+msgid "Reset"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1306
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1307
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1308
+msgid "Perform import"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:132 waeup/kofa/browser/pages.py:899
+#: waeup/kofa/hostels/browser.py:290 waeup/kofa/hostels/browser.py:320
+#: waeup/kofa/students/browser.py:333
+msgid "No item selected."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1373
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1417
+msgid "Process next"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1467
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1469
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1477
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1479
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1480
+msgid "Show"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1496
+msgid "Back"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1497
+msgid "Enter a regular expression here..."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1505
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1520
+msgid "Invalid search expression."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1523
+msgid "No search results found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1531
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1551
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1554
+msgid "Given storage path cannot be used."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1555
+msgid "Error: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1558
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1570
+msgid "Download portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1572
+msgid "Create CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1597 waeup/kofa/students/browser.py:2923
+msgid "Discarded export"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1689
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1701 waeup/kofa/browser/pages.py:1720
+#: waeup/kofa/browser/pages.py:1737 waeup/kofa/browser/pages.py:1741
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1702 waeup/kofa/university/export.py:36
+msgid "Faculties"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1706 waeup/kofa/browser/viewlets.py:419
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1715
+msgid "You are not allowed to remove entire faculties."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1748
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1773 waeup/kofa/browser/pages.py:1783
+#: waeup/kofa/university/export.py:78
+msgid "Departments"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1785 waeup/kofa/browser/pages.py:1833
+#: waeup/kofa/browser/pages.py:1852 waeup/kofa/browser/pages.py:1856
+msgid "Add department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1793 waeup/kofa/browser/viewlets.py:446
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1819
+msgid "You are not allowed to remove entire departments."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1863
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:1866
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1888
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1971
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1919 waeup/kofa/browser/pages.py:1986
+#: waeup/kofa/browser/pages.py:2015 waeup/kofa/browser/pages.py:2019
+msgid "Add course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1920 waeup/kofa/browser/pages.py:1977
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1920 waeup/kofa/browser/pages.py:1991
+#: waeup/kofa/browser/pages.py:2056 waeup/kofa/browser/pages.py:2060
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1929 waeup/kofa/browser/viewlets.py:453
+msgid "Manage department"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:1980
+msgid "You are not allowed to remove certificates."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2031
+msgid "A course with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2037
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2072
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2078
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2113 waeup/kofa/browser/viewlets.py:483
+msgid "Edit course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2155
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2162 waeup/kofa/browser/pages.py:2191
+#: waeup/kofa/browser/pages.py:2228 waeup/kofa/browser/pages.py:2230
+msgid "Add certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2163 waeup/kofa/browser/pages.py:2186
+msgid "Remove selected certificate courses"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2169 waeup/kofa/browser/viewlets.py:513
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2235
+msgid "The chosen certificate course is already part of this certificate."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2239
+msgid "certificate course ${a}_${b} added."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2280 waeup/kofa/browser/viewlets.py:543
+msgid "Edit certificate course"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2283
+msgid "Save and return"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2316
+msgid "Send me a new password"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2364
+msgid "No record found."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:2378 waeup/kofa/students/browser.py:2748
+msgid "You have successfully requested a password for the"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:2382
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:246 waeup/kofa/browser/viewlets.py:281
+#: waeup/kofa/browser/templates/staffsitelayout.pt:28
+#: waeup/kofa/browser/templates/studentsitelayout.pt:32
+msgid "Login"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:257
+msgid "You logged in."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:266
+msgid "Your personal data record is outdated. Please update."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:300
+msgid "Your account has been temporarily deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:307
+msgid "Your account has been deactivated."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:309
+msgid "You entered invalid credentials."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:324
+msgid "You have been logged out. Thanks for using WAeUP Kofa!"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:365
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:371 waeup/kofa/browser/pages.py:652
+msgid "Send message now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:390 waeup/kofa/browser/pages.py:423
+#: waeup/kofa/browser/pages.py:669 waeup/kofa/students/browser.py:489
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:410
+msgid "Send now"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:425
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:450
+msgid "<h1>Welcome to WAeUP.Kofa</h1>"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:508 waeup/kofa/hostels/browser.py:228
+#: waeup/kofa/students/viewlets.py:168 waeup/kofa/students/viewlets.py:252
+#: waeup/kofa/students/viewlets.py:321 waeup/kofa/students/viewlets.py:337
+#: waeup/kofa/students/viewlets.py:396 waeup/kofa/students/viewlets.py:463
+msgid "Manage"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:509 waeup/kofa/browser/viewlets.py:190
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:517
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:554 waeup/kofa/browser/pages.py:556
+#: waeup/kofa/browser/viewlets.py:549
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:578
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:599
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:633
+msgid "User settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:650 waeup/kofa/students/browser.py:470
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:679 waeup/kofa/browser/viewlets.py:369
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:692 waeup/kofa/browser/viewlets.py:385
+msgid "My Roles"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:723
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:750
+msgid "View portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:763
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:764 waeup/kofa/browser/pages.py:814
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:766 waeup/kofa/browser/pages.py:793
+#: waeup/kofa/browser/pages.py:826
+msgid "Add session configuration"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:805
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:817
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:830
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:841
+msgid "The session chosen already exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:863
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:910
+msgid "OSError: The file could not be deleted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:913
+msgid "Successfully deleted: ${a}"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:924
+msgid "Processed Files"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:935
+msgid "Upload portal data as CSV file"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:938
+msgid "Upload"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:984
+msgid "Import Manager"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:986
+msgid "${a}: ${b} uploaded"
+msgstr ""
+
+#: waeup/kofa/browser/pages.py:99
+msgid "No local role selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pages.py:990
+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/pdf.py:153
+msgid "Date:"
+msgstr ""
+
+#: waeup/kofa/browser/pdf.py:204 waeup/kofa/browser/pdf.py:461
+#: waeup/kofa/browser/pdf.py:471
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:12
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:16
+msgid "Date"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:522
+msgid "${footer_text} Page ${num1} of ${num2}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/browser/pdf.py:791
+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
+#: waeup/kofa/students/browser_templates/exportconfig_course.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:6
+msgid "Certificate Courses"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:29
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:35
+#: waeup/kofa/students/browser_templates/transcript.pt:36
+msgid "Semester"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:30
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:36
+msgid "Cert. Course"
+msgstr ""
+
+#: waeup/kofa/browser/templates/certificatepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:37
+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:8
+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:34
+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/datacenterexportpage.pt:44
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:4
+msgid "Export Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:45
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:5
+msgid "Exporter"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:46
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:6
+msgid "Filter"
+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: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:16
+msgid "Sample Record 1"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:17
+msgid "Sample Record 2"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:18
+msgid "Sample Record 3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:41
+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:24
+#: waeup/kofa/browser/templates/datacenterimport4page.pt:13
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:37
+#: waeup/kofa/browser/templates/datacenterimport2page.pt:15
+msgid "Header"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:38
+msgid "change to"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterimport3page.pt:39
+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:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:14
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:36
+msgid "Reload page"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:12
+msgid "Overwrite contents in new location?"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:3
+#: waeup/kofa/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacentermanagepage.pt:7
+msgid "Copy existing data to new storage?"
+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:18
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:10
+#: waeup/kofa/browser/templates/datacenterimport1page.pt:14
+msgid "Datasets"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenterpage.pt:19
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:11
+msgid "Upload Date"
+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:113
+msgid "Download"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:14
+msgid "File:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:22
+msgid "Processor:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:35
+msgid "Import Mode:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:59
+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:67
+msgid "Available Processors (Importers)"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:72
+msgid "Processor"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:73
+msgid "Required Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:74
+msgid "Optional Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:75
+msgid "Non-Schema Fields"
+msgstr ""
+
+#: waeup/kofa/browser/templates/datacenteruploadpage.pt:76
+msgid "CSV Skeleton File"
+msgstr ""
+
+#: waeup/kofa/browser/templates/default_waeup_display_form.pt:6
+msgid "Uploaded Files"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:101
+#: waeup/kofa/browser/templates/facultymanagepage.pt:75
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:97
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:112
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:91
+msgid "Local Role"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:16
+#: waeup/kofa/browser/templates/userscontainerpage.pt:7
+#: waeup/kofa/browser/templates/facultymanagepage.pt:16
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:7
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:17
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:16
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:6
+#: waeup/kofa/browser/templates/facultymanagepage.pt:6
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:5
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:7
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:9
+msgid "Settings"
+msgstr ""
+
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:99
+#: waeup/kofa/browser/templates/userscontainerpage.pt:5
+#: waeup/kofa/browser/templates/facultymanagepage.pt:73
+#: waeup/kofa/browser/templates/configurationmanagepage.pt:31
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:110
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:89
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:22
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:39
+msgid "Name"
+msgstr ""
+
+#: waeup/kofa/browser/templates/facultypage.pt:1
+msgid "There are no subobjects registered yet."
+msgstr ""
+
+#: waeup/kofa/browser/templates/fileupload.pt:26
+#: waeup/kofa/students/browser_templates/imageupload.pt:20
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:32
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:13
+#: waeup/kofa/browser/templates/usereditformpage.pt:9
+#: waeup/kofa/students/browser_templates/basepage.pt:14
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:18
+#: waeup/kofa/students/browser_templates/requestpwmailsent.pt:27
+#: waeup/kofa/students/browser_templates/change_password.pt:10
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:38
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:31
+#: waeup/kofa/applicants/browser_templates/applicantregemailsent.pt:27
+msgid "Password:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:24
+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:36
+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:39
+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:44
+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:48
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/kofa/browser/templates/loginpage.pt:5
+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/reportscontainercreate.pt:22
+msgid "Type of report to create"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:4
+msgid "Report Number"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:41
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:76
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:36
+msgid "<img src=\"${DYNAMIC_CONTENT}\" /> Reload"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:5
+msgid "Description"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:51
+msgid "Create new report"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:6
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:47
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:7
+#: waeup/kofa/accesscodes/browser_templates/batchcontainer.pt:15
+msgid "Creator"
+msgstr ""
+
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:7
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:48
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:8
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:91
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:47
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:16
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:17
+msgid "Department"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:17
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:46
+msgid "Type"
+msgstr ""
+
+#: waeup/kofa/browser/templates/searchpage.pt:9
+#: waeup/kofa/students/browser_templates/containerpage.pt:24
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:18
+msgid "Search Results"
+msgstr ""
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:10
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:62
+msgid "Comment"
+msgstr ""
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:5
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:57
+msgid "Fullname"
+msgstr ""
+
+#: waeup/kofa/browser/templates/showstudentspage.pt:6
+#: waeup/kofa/browser/templates/reportscontainerpage.pt:8
+#: waeup/kofa/browser/templates/datacenterexportpage.pt:49
+#: waeup/kofa/students/browser_templates/exportjobsindex.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:58
+msgid "Status"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:113
+#: waeup/kofa/browser/templates/studentsitelayout.pt:97
+msgid "Back to bottom"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:122
+#: waeup/kofa/browser/templates/studentsitelayout.pt:107
+msgid "Back to top"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:124
+#: waeup/kofa/browser/templates/studentsitelayout.pt:109
+msgid "Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:127
+#: waeup/kofa/browser/templates/studentsitelayout.pt:112
+msgid "Copyright &copy; WAeUP Group 2008-2013"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:34
+#: waeup/kofa/browser/templates/studentsitelayout.pt:38
+msgid "Contact"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:4
+#: waeup/kofa/browser/templates/studentsitelayout.pt:4
+msgid "WAeUP.Kofa - Student Management System"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:40
+#: waeup/kofa/browser/templates/studentsitelayout.pt:44
+msgid "Logout"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:62
+msgid "Student Id:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/staffsitelayout.pt:66
+msgid "State:"
+msgstr ""
+
+#: waeup/kofa/browser/templates/usereditformpage.pt:15
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:24
+#: waeup/kofa/students/browser_templates/change_password.pt:16
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:44
+msgid "Retype Password:"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:180
+msgid "Add"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:320
+msgid "Any link text"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:402 waeup/kofa/students/viewlets.py:160
+msgid "Send email"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:411
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:428 waeup/kofa/browser/viewlets.py:474
+#: waeup/kofa/browser/viewlets.py:504 waeup/kofa/browser/viewlets.py:534
+msgid "Export student data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:437
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:463 waeup/kofa/browser/viewlets.py:493
+#: waeup/kofa/browser/viewlets.py:523
+msgid "Show students"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:558
+msgid "Show logs"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:567
+msgid "Upload data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:576
+msgid "Process data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:585
+msgid "Export data"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:594
+msgid "View processed files"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:667
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/kofa/browser/viewlets.py:95 waeup/kofa/browser/viewlets.py:170
+#: waeup/kofa/students/viewlets.py:329 waeup/kofa/students/viewlets.py:536
+#: waeup/kofa/students/viewlets.py:583
+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:163
+#: waeup/kofa/hostels/browser.py:190
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:133 waeup/kofa/hostels/browser.py:168
+msgid "Clear all hostels"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:171
+msgid "All hostels cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:179
+msgid "Settings have been saved."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:193
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:202
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:204
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:239
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:242 waeup/kofa/hostels/browser.py:270
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:243 waeup/kofa/hostels/browser.py:284
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:244 waeup/kofa/hostels/browser.py:314
+msgid "Release selected beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:245 waeup/kofa/hostels/browser.py:341
+msgid "Clear hostel"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:275
+msgid ""
+"${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:308
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/browser.py:332
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:337
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:344
+msgid "Hostel cleared."
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:357
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/kofa/hostels/browser.py:363
+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/containerpage.pt:21
+#: waeup/kofa/hostels/browser_templates/containermanagepage.pt:37
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:45
+msgid "Id"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/containerpage.pt:23
+msgid "Booked Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:47
+msgid "Number"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:6
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:66
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/kofa/hostels/browser_templates/hostelpage.pt:5
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:19
+msgid "Reserved Beds:"
+msgstr ""
+
+#: waeup/kofa/hostels/export.py:61
+#: waeup/kofa/hostels/browser_templates/hostelmanagepage.pt:9
+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:105
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:111
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:117
+msgid "Reserved Beds"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:128
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:135
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:142
+msgid "Beds for Pre-Study Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:149
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:156
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:163
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:170
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:177
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:188
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:198
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:220
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:226
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:232
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:237
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:238
+msgid "Enter valid student id."
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:253
+msgid ""
+"Either student does not exist or student is not in accommodation session."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/hostels/interfaces.py:260
+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:93
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/kofa/hostels/interfaces.py:99
+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 K"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:63
+msgid "Block L"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:64
+msgid "Block M"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:65
+msgid "Block N"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:66
+msgid "Block O"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:67
+msgid "Block P"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:68
+msgid "Block Q"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:69
+msgid "Block R"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:70
+msgid "Block S"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:71
+msgid "Block T"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:72
+msgid "Block U"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:73
+msgid "Block V"
+msgstr ""
+
+#: waeup/kofa/hostels/vocabularies.py:74
+msgid "Block W"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:207
+msgid "Subject"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:211 waeup/kofa/students/browser.py:1134
+#: waeup/kofa/students/browser.py:1297
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:10
+msgid "Grade"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:368
+msgid "Processor name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:394
+msgid "Email Address:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:401
+msgid "Email to:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:408
+msgid "Subject:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:412
+msgid "Full Name:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:416
+msgid "Text:"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:445 waeup/kofa/interfaces.py:501
+msgid "Public Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:455 waeup/kofa/interfaces.py:456
+msgid "Number of failed logins"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:462
+msgid "Timestamp"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:463
+msgid "Timestamp of last failed login or `None`"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:492
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:97
+#: waeup/kofa/browser/templates/userscontainerpage.pt:4
+#: waeup/kofa/browser/templates/facultymanagepage.pt:71
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:93
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:108
+#: waeup/kofa/applicants/browser_templates/applicantsrootmanagepage.pt:87
+msgid "User Id"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:497
+#: waeup/kofa/students/browser_templates/containerpage.pt:36
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:42
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:48
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:29
+msgid "Full Name"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:507
+msgid "Description/Notice"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:524
+#: waeup/kofa/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:571
+msgid "Name of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:572
+msgid "Sample University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:577
+msgid "Abbreviated Title of University"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:583
+msgid "Skin"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:59
+msgid "new"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:590
+msgid "Content in HTML format"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:60
+msgid "completed"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:602
+msgid "Name of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:608
+msgid "Email Address of Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:61
+msgid "pending"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:615
+msgid "Subject of Email to Administrator"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:616
+msgid "Kofa Contact"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:62
+msgid "active"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:621
+msgid "SMTP mailer to use when sending mail"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:628
+msgid "Captcha used for public registration pages"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:63
+msgid "assigned"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:635
+msgid "Carry-over Course Registration"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:64
+msgid "callbacks"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:644 waeup/kofa/interfaces.py:690
+msgid "Academic Session"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:658
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:664
+msgid "Bed Booking Fee"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:670
+msgid "Rent"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:676
+msgid "Clearance enabled"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:92
+msgid "clearance started"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:93
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:94
+msgid "cleared"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:95
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:96
+msgid "courses registered"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:97
+msgid "courses validated"
+msgstr ""
+
+#: waeup/kofa/interfaces.py:98
+msgid "returning"
+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. Proceed to the login page and enter your "
+"credentials."
+msgstr ""
+
+#: waeup/kofa/objecthistory.py:74
+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:782
+#: waeup/kofa/students/interfaces.py:806
+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:789
+#: waeup/kofa/students/interfaces.py:813
+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/applicants/browser_templates/applicanteditpage.pt:92
+#: 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:853
+msgid "System"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1024
+msgid "You must select a session for the level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1033
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1035 waeup/kofa/students/browser.py:2477
+msgid "This level exists."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1045
+msgid "No study level selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1073
+msgid "You are not allowed to view the transcript."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1088
+msgid "${a}: Transcript Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1106
+msgid "You are not allowed to download the transcript."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1121 waeup/kofa/students/viewlets.py:366
+msgid "Academic Transcript"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1125
+msgid "CERTIFIED TRUE COPY"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1129 waeup/kofa/students/browser.py:1289
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:35
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:25
+msgid "Sem."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1132 waeup/kofa/students/interfaces.py:623
+#: waeup/kofa/university/interfaces.py:152
+#: waeup/kofa/students/browser_templates/transcript.pt:39
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:19
+msgid "Credits"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1133 waeup/kofa/students/browser.py:1296
+#: waeup/kofa/students/interfaces.py:634 waeup/kofa/students/interfaces.py:667
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:20
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:32
+msgid "Score"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1161 waeup/kofa/students/viewlets.py:203
+msgid "Transfer student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1171
+msgid "Transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1175
+msgid "Current level does not match certificate levels."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1177
+msgid "Former study course record incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1179
+msgid "Maximum number of transfers exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1181
+msgid "Successfully transferred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1191
+msgid "Revert previous transfer"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1196
+msgid "No previous transfer."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1201
+msgid "Revert now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1204
+msgid "Previous transfer reverted."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1235
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1256
+msgid "Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1262
+msgid "1st Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1268
+msgid "2nd Semester Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1274
+msgid "Level Courses"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1283
+msgid "Course Registration Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1292
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:38
+msgid "Dept."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1293
+#: waeup/kofa/browser/templates/searchpage.pt:15
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/kofa/applicants/browser_templates/applicantcontainerstatistics.pt:22
+msgid "Faculty"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1294
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:40
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:30
+msgid "Cred."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1342 waeup/kofa/students/browser.py:1481
+#: waeup/kofa/students/browser.py:1492 waeup/kofa/students/browser.py:2602
+msgid "Add course ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1360 waeup/kofa/students/browser.py:2507
+msgid "No valid course code entered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1366 waeup/kofa/students/browser.py:2512
+msgid "Course not found."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1383
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1397 waeup/kofa/students/browser.py:2536
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:141
+msgid "Clearance is disabled for this session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1427 waeup/kofa/students/browser.py:1452
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1431
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1433 waeup/kofa/students/browser.py:1464
+msgid "Student is in the wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1457
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1461
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1516
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:152
+msgid "Total credits exceed ${a}."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1541
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1574
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:159
+msgid "The ticket exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:161
+msgid "Successfully added ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1622
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1630 waeup/kofa/students/browser.py:1680
+#: waeup/kofa/students/browser.py:1711
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1640 waeup/kofa/students/browser.py:1931
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1646
+msgid "You have not yet booked accommodation."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1670
+msgid "Add previous session online payment"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1675
+msgid "No previous payment to be made."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1708
+msgid "Add balance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:182 waeup/kofa/students/export.py:197
+#: waeup/kofa/students/reports.py:154 waeup/kofa/students/viewlets.py:688
+msgid "Students"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1844
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1856
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1863
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1895
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1897
+msgid "Create bed ticket"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1914
+msgid "Outside booking period: ${a} - ${b}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1917
+msgid "Outside booking period."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1921
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1925
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1936
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1949 waeup/kofa/students/browser.py:2268
+#: waeup/kofa/students/browser.py:2410
+msgid "Activation code is invalid."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1961 waeup/kofa/students/browser.py:1976
+msgid "System error: Please contact the adminsitrator."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:1981 waeup/kofa/students/browser.py:2117
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1987 waeup/kofa/students/browser.py:2271
+#: waeup/kofa/students/browser.py:2414
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1990 waeup/kofa/students/browser.py:2275
+#: waeup/kofa/students/browser.py:2417 waeup/kofa/students/browser.py:2662
+msgid "invalidated"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:1995 waeup/kofa/students/browser.py:2279
+#: waeup/kofa/students/browser.py:2422
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2007 waeup/kofa/students/browser.py:2131
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2016
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2033
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2052
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:206 waeup/kofa/students/viewlets.py:119
+#: waeup/kofa/students/viewlets.py:751
+msgid "Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2061
+msgid "Bed Allocation Slip"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:208
+msgid "Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2089
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2095
+msgid "Student can't be relocated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2139
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:214 waeup/kofa/students/viewlets.py:125
+#: waeup/kofa/students/viewlets.py:752
+msgid "Payments"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2158
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2170
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2185 waeup/kofa/students/viewlets.py:545
+msgid "Change password"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2199
+msgid "Password changed."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2211
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2226 waeup/kofa/students/viewlets.py:569
+#: waeup/kofa/students/workflow.py:48
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2230
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2248
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2252
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2256
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2283
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:229 waeup/kofa/students/viewlets.py:131
+msgid "Accommodation"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2293
+msgid "Edit clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2314 waeup/kofa/students/browser.py:2328
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2322
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2339 waeup/kofa/students/browser.py:2373
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2342 waeup/kofa/students/browser.py:2376
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2351 waeup/kofa/students/workflow.py:62
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2352
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2355
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2367
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:238
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2385
+msgid "Start session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2389
+msgid "Start now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2398
+msgid "You are not entitled to start session."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2435
+msgid "An error occurred, please contact the system administrator."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2437
+msgid "Session started."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2456
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2468
+msgid "Create course list now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2479
+msgid "Your data are incomplete"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2524
+msgid "Edit course list of ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2565
+msgid "You are a postgraduate student, your course list can't bee registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:257
+msgid "Student Section"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2573
+msgid "Maximum credits of ${a} exceeded."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2578
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2582
+msgid "Register course list"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2618
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2621
+msgid "Set"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2636
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2644
+msgid "Access code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2647
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2653
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2659
+msgid "Access code has already been used."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:2668
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2680
+msgid "Request password for first-time login"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2723 waeup/kofa/students/browser.py:2734
+msgid "No student record found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2768
+msgid "Your password request was successful."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2784 waeup/kofa/students/interfaces.py:417
+#: waeup/kofa/students/interfaces.py:508
+#: waeup/kofa/browser/templates/showstudentspage.pt:8
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:60
+msgid "Current Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2785 waeup/kofa/students/interfaces.py:424
+#: waeup/kofa/students/interfaces.py:456 waeup/kofa/students/interfaces.py:514
+#: waeup/kofa/browser/templates/showstudentspage.pt:9
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:61
+msgid "Current Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:282
+msgid "Only year dates allowed (e.g. 2011)."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2836
+msgid "in any session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2842
+msgid "at any level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:287 waeup/kofa/students/browser.py:331
+#: waeup/kofa/students/browser.py:2633 waeup/kofa/students/browser.py:2845
+msgid "No student found."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2878 waeup/kofa/students/interfaces.py:539
+#: waeup/kofa/students/interfaces.py:714 waeup/kofa/students/utils.py:224
+#: waeup/kofa/students/browser_templates/transcript.pt:35
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:8
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:14
+msgid "Session"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2879 waeup/kofa/students/utils.py:226
+#: waeup/kofa/university/interfaces.py:340
+#: waeup/kofa/browser/templates/certificatepage.pt:28
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:34
+#: waeup/kofa/students/browser_templates/transcript.pt:34
+msgid "Level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2936
+msgid "Configure student data export"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2944
+msgid "All Sessions"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2951
+msgid "All Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:2958
+msgid "All Modes"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:298 waeup/kofa/students/viewlets.py:144
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3022
+msgid "Export started for students with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:3113
+msgid "Export started for course tickets with"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:358 waeup/kofa/students/viewlets.py:151
+msgid "Add student"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:366
+msgid "Student record created."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:380
+msgid "Set temporary password for ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:402
+msgid "Login now"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:406
+msgid "Login as ${a}"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:412
+msgid "You successfully logged in as student."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:430
+msgid "${a}: Base Data (account deactivated)"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:432
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:509
+msgid "Admission Letter of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:527
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:576
+msgid "Trigger registration transition"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:616
+msgid "Student account has been activated."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:635
+msgid "Student account has been deactivated."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:670
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:700 waeup/kofa/students/viewlets.py:109
+#: waeup/kofa/students/viewlets.py:749
+#: waeup/kofa/students/browser_templates/clearanceeditpage.pt:6
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:706
+msgid "Clearance Slip of"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:715
+msgid "Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:716
+msgid "Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:723
+msgid "Date, Student Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:724
+msgid "Date, Clearance Officer Signature"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:745
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:789
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:791 waeup/kofa/students/browser.py:826
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:803 waeup/kofa/students/viewlets.py:277
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:815
+msgid "Save comment and reject clearance now"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:819
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/kofa/students/browser.py:823
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:856
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:869
+msgid "Manage personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:884
+msgid "Edit personal data"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:887
+msgid "Save/Confirm"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:914
+msgid "${a}: Study Course"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/browser.py:917
+msgid "${a}: Previous Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:945
+msgid "First Study Course, "
+msgstr ""
+
+#: waeup/kofa/students/browser.py:947
+msgid "Second Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:952
+msgid "First Study Course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:963
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:966 waeup/kofa/students/browser.py:1039
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:967 waeup/kofa/students/browser.py:1019
+msgid "Add study level"
+msgstr ""
+
+#: waeup/kofa/students/browser.py:999
+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/clearanceeditpage.pt:8
+msgid "Scans"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:31
+#: waeup/kofa/applicants/browser_templates/applicantscontainermanagepage.pt:38
+msgid "Select all"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:10
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:10
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:10
+msgid "with registration number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:11
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:11
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:12
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:12
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:78
+msgid "in session"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:13
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:13
+msgid "in department"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:14
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:14
+msgid "in study mode"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:15
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:15
+msgid "with deactivated account (search term omitted)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:33
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:39
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:27
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:34
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:40
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:35
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:41
+#: waeup/kofa/students/browser_templates/paymentsmanagepage.pt:15
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:25
+#: waeup/kofa/applicants/browser_templates/applicanteditpage.pt:95
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "State"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:5
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:5
+msgid "for students"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:8
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:8
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:8
+msgid "with id"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/containerpage.pt:9
+#: waeup/kofa/students/browser_templates/containermanagepage.pt:9
+#: waeup/kofa/applicants/browser_templates/applicantssearchpage.pt:9
+msgid "with name"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/courseticketmanagepage.pt:18
+msgid "Weight"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:20
+msgid "The activation code can be found on the payment slip."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:34
+msgid ""
+"Your data have been approved. You can continue by clicking the button below."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig.pt:48
+msgid "Current Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:1
+#: waeup/kofa/students/browser_templates/exportconfig.pt:1
+msgid ""
+"<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."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:12
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:2
+#: waeup/kofa/students/browser_templates/exportconfig.pt:12
+msgid "Exporter (Data Type):"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:24
+#: waeup/kofa/students/browser_templates/exportconfig.pt:24
+msgid "Current Session:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_certificate.pt:36
+#: waeup/kofa/students/browser_templates/exportconfig.pt:36
+msgid "Current Level:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/exportconfig_course.pt:14
+msgid "Level Session:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:50
+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:93
+#: 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:94
+#: 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:90
+#: waeup/kofa/applicants/browser_templates/applicantdisplaypage.pt:46
+msgid "Payment Id"
+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: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:32
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/setpassword.pt:33
+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/studentsreportgeneratorpage.pt:10
+msgid "Study Modes Group:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.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/browser_templates/studentsreportgeneratorpage.pt:21
+msgid "Set study modes filter."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.pt:33
+msgid "<option value=\"${DYNAMIC_CONTENT}\"> ${DYNAMIC_CONTENT} </option>"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studentsreportgeneratorpage.pt:37
+msgid "Select academic session to report."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:6
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:9
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:50
+msgid "Previous Study Courses:"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:64
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:30
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:69
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:35
+msgid "Level Code"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:70
+#: waeup/kofa/students/browser_templates/studycoursemanagepage.pt:36
+msgid "Level Title"
+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: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:39
+msgid "Fact."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:31
+msgid "Requ."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:43
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:21
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:34
+msgid "CO"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:44
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:35
+msgid "Auto"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:6
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:87
+#: waeup/kofa/students/browser_templates/studyleveleditpage.pt:62
+msgid "Enter valid course code."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:93
+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:28
+msgid "Dep."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:29
+msgid "Fac."
+msgstr ""
+
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:58
+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/export.py:220
+msgid "Student Study Courses"
+msgstr ""
+
+#: waeup/kofa/students/export.py:250
+msgid "Student Study Levels"
+msgstr ""
+
+#: waeup/kofa/students/export.py:275
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:9
+#: waeup/kofa/students/browser_templates/studylevelmanagepage.pt:30
+#: waeup/kofa/students/browser_templates/studylevelpage.pt:20
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:309
+msgid "Student Payments"
+msgstr ""
+
+#: waeup/kofa/students/export.py:346
+msgid "Payment Data for Bursary"
+msgstr ""
+
+#: waeup/kofa/students/export.py:377
+msgid "Bed Tickets"
+msgstr ""
+
+#: waeup/kofa/students/export.py:413
+msgid "Student Payments Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:442
+msgid "Student Study Levels Overview"
+msgstr ""
+
+#: waeup/kofa/students/export.py:471
+msgid "Combo Card Data"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:191
+msgid "Reasons for Deactivation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:193
+msgid ""
+"This message will be shown if and only if deactivated students try to login."
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:232 waeup/kofa/students/interfaces.py:365
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:239
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:245
+msgid "Email"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:280
+msgid "Officer's Comment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:285
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:291
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:304
+msgid "Nationality"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:313
+msgid "Employer"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:323
+msgid "Updated"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:329 waeup/kofa/students/interfaces.py:347
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:376
+msgid "Registr. or Matric. Number"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:396 waeup/kofa/students/interfaces.py:450
+#: waeup/kofa/browser/templates/showstudentspage.pt:7
+#: waeup/kofa/students/browser_templates/filterstudentspage.pt:59
+msgid "Certificate"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:402 waeup/kofa/students/interfaces.py:482
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:409 waeup/kofa/students/interfaces.py:463
+#: waeup/kofa/students/interfaces.py:489
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:431 waeup/kofa/students/interfaces.py:502
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:438
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:520
+msgid "Bypass validation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:525 waeup/kofa/students/interfaces.py:552
+msgid "Validated by"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:545
+msgid "Verdict"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:558
+msgid "Validation Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:564
+msgid "Total Credits"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:602
+msgid "Faculty Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:607
+msgid "Department Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:612
+#: waeup/kofa/university/interfaces.py:164
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:618
+#: waeup/kofa/university/interfaces.py:158
+msgid "Passmark"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:628
+#: waeup/kofa/browser/templates/certificatepage.pt:33
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:39
+msgid "Required"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:640
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:646
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:657
+#: waeup/kofa/university/interfaces.py:334
+#: waeup/kofa/university/interfaces.py:369
+#: waeup/kofa/students/browser_templates/transcript.pt:37
+msgid "Course"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:673
+msgid "Level Session"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:702
+msgid "Allocated Bed"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:708
+msgid "Requested Bed Type"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:721
+#: waeup/kofa/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:727
+msgid "Booking Activation Code"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:748
+msgid "Current Session Payment"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:754 waeup/kofa/students/interfaces.py:795
+#: waeup/kofa/students/interfaces.py:819
+msgid "Payment Level"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:825
+msgid "Balance Amount"
+msgstr ""
+
+#: waeup/kofa/students/interfaces.py:829
+msgid "Balance in Naira "
+msgstr ""
+
+#: waeup/kofa/students/reports.py:176
+msgid "Create students report"
+msgstr ""
+
+#: waeup/kofa/students/reports.py:196
+msgid "New report is being created in background"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:122
+msgid "Name:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:141
+msgid "Study Course:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:148 waeup/kofa/students/utils.py:734
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:157 waeup/kofa/students/utils.py:727
+#: waeup/kofa/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:166 waeup/kofa/students/utils.py:741
+msgid "Entry Session:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:174
+msgid "Date of Birth:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:261
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:338
+msgid "The previous session must not fall below your entry session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:343 waeup/kofa/students/utils.py:346
+msgid "This is not a previous session."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:363 waeup/kofa/students/utils.py:402
+msgid "Study course data are incomplete."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:417
+msgid "no bed allocated"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:419
+msgid "Amount could not be determined."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:452
+msgid "Amount must be greater than 0."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:516
+msgid ""
+"This is to inform you that you have been provisionally admitted into ${a} as "
+"follows:"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/utils.py:556
+msgid "Your Kofa student record was created on ${a}."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:608 waeup/kofa/students/viewlets.py:76
+#: waeup/kofa/students/viewlets.py:104
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:7
+msgid "Base Data"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:628
+msgid "Workflow History"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:721
+msgid "Course of Study:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:748
+msgid "Entry Mode:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:755
+msgid "Cumulative GPA:"
+msgstr ""
+
+#: waeup/kofa/students/utils.py:786
+msgid "Error in image file."
+msgstr ""
+
+#: waeup/kofa/students/utils.py:814
+msgid "(not yet)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:114 waeup/kofa/students/viewlets.py:750
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:136 waeup/kofa/students/viewlets.py:755
+#: waeup/kofa/accesscodes/browser_templates/searchpage.pt:27
+msgid "History"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:177
+msgid "Trigger transition"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:186
+msgid "Login as student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:195
+msgid "Download admission letter"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:212
+msgid "Deactivate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:224 waeup/kofa/students/viewlets.py:244
+msgid "'A history message will be added. Are you sure?'"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:232
+msgid "Activate account"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:260 waeup/kofa/students/workflow.py:76
+msgid "Clear student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:295
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:304 waeup/kofa/students/viewlets.py:313
+msgid "View"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:351
+msgid "Transcript"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:382
+msgid "Reactivate this study course (revert previous transfer)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:411 waeup/kofa/students/workflow.py:160
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:429
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:448
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:510
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:519
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:528
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:554
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:598
+msgid "Start new session"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:612
+msgid "Add course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:631
+msgid "Edit course list"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:649
+msgid "Add current session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:657
+msgid "Add previous session payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:672
+msgid "Add balance payment ticket"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:754
+msgid "Accommodation Data"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:766
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:780
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:786
+msgid "Could not determine file type."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:791
+msgid "${a} file extension expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:797
+msgid "Only the following extensions are allowed: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/kofa/students/viewlets.py:806
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:824
+#: waeup/kofa/browser/templates/datacenterlogspage.pt:10
+#: waeup/kofa/browser/templates/datacenterpage.pt:17
+#: waeup/kofa/browser/templates/datacenterfinishedpage.pt:9
+#: waeup/kofa/accesscodes/browser_templates/reimportbatchpage.pt:11
+msgid "File"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:825
+msgid "Scan"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:848
+msgid "Upload new file"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:849
+msgid "Delete attachment"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:886
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:90
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:900
+#: waeup/kofa/students/browser_templates/basemanagepage.pt:9
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:911
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:926 waeup/kofa/students/viewlets.py:937
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/kofa/students/viewlets.py:927 waeup/kofa/students/viewlets.py:938
+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: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:10
+#: waeup/kofa/browser/templates/departmentpage.pt:3
+#: waeup/kofa/browser/templates/certificatepage.pt:22
+#: waeup/kofa/browser/templates/certificatemanagepage.pt:28
+msgid "Courses"
+msgstr ""
+
+#: waeup/kofa/university/export.py:165
+#: waeup/kofa/browser/templates/departmentmanagepage.pt:13
+#: 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:146
+msgid "Title of course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:171
+msgid "Former Course"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:172
+msgid ""
+"If this attribute is being set all certificate courses referring to this "
+"course will be automatically deleted."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:189
+msgid "Enter unique course code which will become part of the URL."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:209
+msgid "Unnamed Certificate"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:214
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:221
+msgid "Start Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:228
+msgid "End Level"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:235
+msgid "Application Category"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:242
+msgid "Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:247
+msgid "Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:252
+msgid "Foreigner Initial School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:257
+msgid "Foreigner Returning School Fee"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:262
+msgid "Installment Ratio"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:269
+msgid "Custom Textline 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:274
+msgid "Custom Textline 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:279
+msgid "Custom Float 1 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:284
+msgid "Custom Float 2 (not used)"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:296
+#: waeup/kofa/university/interfaces.py:298
+msgid "Start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:300
+#: waeup/kofa/university/interfaces.py:303
+msgid "Study mode, start level and end level must correspond."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:313
+msgid "Enter unique certificate code which will become part of the URL."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:347
+msgid "Registration required"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:45
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:46
+#: waeup/kofa/university/interfaces.py:94
+#: waeup/kofa/university/interfaces.py:147
+msgid "Unnamed"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:51
+#: waeup/kofa/university/interfaces.py:99
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:67
+#: waeup/kofa/university/interfaces.py:118
+msgid "This code will become part of the URL."
+msgstr ""
+
+#: waeup/kofa/university/interfaces.py:93
+msgid "Name of department"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:29
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:30
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:31
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:32
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:33
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:34
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:35
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:36
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:37
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:38
+msgid "900 (Year 9)"
+msgstr ""
+
+#: waeup/kofa/university/vocabularies.py:39
+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:186
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/kofa/utils/utils.py:239
+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:18
+msgid "Don't forget to save the form after adding or deleting items."
+msgstr ""
+
+#. Default: "Remove selected items"
+#: waeup/kofa/widgets/sequencewidget.pt:21
+msgid "remove-selected-items"
+msgstr ""
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/zope.pot
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/zope.pot	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/locales/zope.pot	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/mail.zcml
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/mail.zcml	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/mail.zcml	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/maintenance.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/maintenance.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/maintenance.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/mandates/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/mandates/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/mandates/__init__.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/mandates/browser.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/mandates/browser.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/mandates/browser.py	(revision 10886)
@@ -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
+##
+"""Views for mandates.
+"""
+import grok
+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.flash(_('Misuse'))
+            return
+        mandates = grok.getSite()['mandates']
+        mandate = mandates.get(mandate_id, None)
+        if mandate is None:
+            self.flash(_('No mandate.'))
+            return
+        msg = mandate.execute()
+        self.flash(msg)
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context))
+        return
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/mandates/container.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/mandates/container.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/mandates/container.py	(revision 10886)
@@ -0,0 +1,66 @@
+## $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)
+
+    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
+        for mandate in self.keys():
+            if self[mandate].expires < datetime.utcnow():
+                del self[mandate]
+                num_deleted += 1
+        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 faculties 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/tags/0.2/src/waeup/kofa/mandates/interfaces.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/mandates/interfaces.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/mandates/interfaces.py	(revision 10886)
@@ -0,0 +1,46 @@
+## $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.
+
+    """
+
+    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/tags/0.2/src/waeup/kofa/mandates/mandate.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/mandates/mandate.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/mandates/mandate.py	(revision 10886)
@@ -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
+##
+"""
+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()
+
+    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.')
+        if self.expires < datetime.utcnow():
+            msg = _('Mandate expired.')
+        if self._setPassword():
+            msg = _('Password has been successfully set. '
+                    'Proceed to the login page and enter your credentials.')
+            username = IUserAccount(self.params['user']).name
+            grok.getSite().logger.info(
+                'PasswordMandate used: %s ' % username)
+        del self.__parent__[self.mandate_id]
+        return msg
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/mandates/tests.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/mandates/tests.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/mandates/tests.py	(revision 10886)
@@ -0,0 +1,179 @@
+## $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
+from waeup.kofa.testing import (FunctionalLayer, FunctionalTestCase)
+
+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())
+            )
+        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()
+        self.app['mandates'].addMandate(mandate)
+        msg = mandate.execute()
+        self.assertEqual(msg, u'Wrong mandate parameters.')
+        # Add and execute an expired mandate.
+        mandate = PasswordMandate(days=0)
+        self.app['mandates'].addMandate(mandate)
+        msg = mandate.execute()
+        self.assertEqual(msg, u'Mandate expired.')
+        # Add and execute a perfect mandate
+        mandate = PasswordMandate()
+        mandate.params['user'] = student
+        mandate.params['password'] = 'mypwd1'
+        self.app['mandates'].addMandate(mandate)
+        msg = mandate.execute()
+        # Password has been set.
+        self.assertEqual(msg, 'Password has been successfully set. Proceed to '
+            'the login page and enter your credentials.')
+        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', 'bobssecret')
+        officer = self.app['users']['bob']
+        mandate = PasswordMandate()
+        mandate.params['user'] = officer
+        mandate.params['password'] = 'mypwd1'
+        self.app['mandates'].addMandate(mandate)
+        msg = mandate.execute()
+        # Password has been set.
+        self.assertEqual(msg, 'Password has been successfully set. Proceed to '
+            'the login page and enter your credentials.')
+        self.assertTrue(IUserAccount(officer).checkPassword('mypwd1'))
+        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 = mandate.execute()
+        # Password has been set.
+        self.assertEqual(msg, 'Password has been successfully set. Proceed to '
+            'the login page and enter your credentials.')
+        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)
+        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'])
+
+    def test_browser(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. Proceed to '
+                        'the login page and enter your credentials.'
+            in self.browser.contents)
+        self.assertTrue(IUserAccount(student).checkPassword('mypwd1'))
+        # All mandates have been removed.
+        self.assertEqual(len(self.app['mandates'].keys()), 0)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/meta.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/meta.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/meta.py	(revision 10886)
@@ -0,0 +1,107 @@
+## $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 martian
+from grok import components
+from grok.meta import IndexesSetupSubscriber
+from martian.error import GrokError
+from zope import component
+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)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/meta.zcml
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/meta.zcml	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/meta.zcml	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/objecthistory.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/objecthistory.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/objecthistory.py	(revision 10886)
@@ -0,0 +1,108 @@
+## $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):
+        """Add the message (history entry) in msg.
+
+        Any message will be stored with a timestamp and the current
+        user (principal).
+        """
+        msgs = self._getMessages()
+        tz = getUtility(IKofaUtils).tzinfo
+        timestamp = now(tz).strftime("%Y-%m-%d %H:%M:%S %Z")
+        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
+        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        by = translate(_('by'),'waeup.kofa',target_language=portal_language)
+        msg = translate(msg,'waeup.kofa',target_language=portal_language)
+        msg = u'%s - %s %s %s' % (timestamp, msg, by, usertitle)
+        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/tags/0.2/src/waeup/kofa/payments/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/payments/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/payments/__init__.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/payments/catalog.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/payments/catalog.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/payments/catalog.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/payments/container.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/payments/container.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/payments/container.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/payments/interfaces.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/payments/interfaces.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/payments/interfaces.py	(revision 10886)
@@ -0,0 +1,146 @@
+## $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.university.vocabularies import ContextualDictSourceFactoryBase
+from waeup.kofa.interfaces import (
+    IKofaObject, SimpleKofaVocabulary, academic_sessions_vocab)
+from waeup.kofa.interfaces import MessageFactory as _
+
+payment_states = SimpleKofaVocabulary(
+    (_('Not yet paid'),'unpaid'),
+    (_('Paid'),'paid'),
+    (_('Failed'),'failed'),
+    )
+
+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 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,
+        )
+
+    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')
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/payments/payment.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/payments/payment.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/payments/payment.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/payments/tests.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/payments/tests.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/payments/tests.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/permissions.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/permissions.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/permissions.py	(revision 10886)
@@ -0,0 +1,496 @@
+## $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):
+    """Everyone-can-do-this-permission.
+
+    This permission is meant to be applied to objects/views/pages
+    etc., that should be usable/readable by everyone.
+
+    We need this to be able to tune default permissions more
+    restrictive and open up some dedicated objects like the front
+    page.
+    """
+    grok.name('waeup.Public')
+
+class Anonymous(grok.Permission):
+    """Only-anonymous-can-do-this-permission.
+    """
+    grok.name('waeup.Anonymous')
+
+class Authenticated(grok.Permission):
+    """Only-logged-in-users-can-do-this-permission.
+    """
+    grok.name('waeup.Authenticated')
+
+class ViewAcademicsPermission(grok.Permission):
+    grok.name('waeup.viewAcademics')
+
+class ManageAcademicsPermission(grok.Permission):
+    grok.name('waeup.manageAcademics')
+
+class ManagePortal(grok.Permission):
+    grok.name('waeup.managePortal')
+
+class ManageUsers(grok.Permission):
+    grok.name('waeup.manageUsers')
+
+class ShowStudents(grok.Permission):
+    grok.name('waeup.showStudents')
+
+class EditScores(grok.Permission):
+    grok.name('waeup.editScores')
+
+class EditUser(grok.Permission):
+    grok.name('waeup.editUser')
+
+class ManageDataCenter(grok.Permission):
+    grok.name('waeup.manageDataCenter')
+
+class ImportData(grok.Permission):
+    grok.name('waeup.importData')
+
+class ExportData(grok.Permission):
+    grok.name('waeup.exportData')
+
+class ExportPaymentsOverview(grok.Permission):
+    grok.name('waeup.exportPaymentsOverview')
+
+class ExportBursaryData(grok.Permission):
+    grok.name('waeup.exportBursaryData')
+
+class ViewTranscript(grok.Permission):
+    grok.name('waeup.viewTranscript')
+
+class ManagePortalConfiguration(grok.Permission):
+    grok.name('waeup.managePortalConfiguration')
+
+class ManageACBatches(grok.Permission):
+    grok.name('waeup.manageACBatches')
+
+# Local Roles
+class ApplicationsManager(grok.Role):
+    grok.name('waeup.local.ApplicationsManager')
+    grok.title(u'Applications Manager')
+    grok.permissions('waeup.viewAcademics')
+
+class DepartmentManager(grok.Role):
+    grok.name('waeup.local.DepartmentManager')
+    grok.title(u'Department Manager')
+    grok.permissions('waeup.manageAcademics',
+                     'waeup.showStudents',
+                     'waeup.exportData')
+
+class DepartmentOfficer(grok.Role):
+    grok.name('waeup.local.DepartmentOfficer')
+    grok.title(u'Department Officer')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportPaymentsOverview')
+
+class ClearanceOfficer(grok.Role):
+    """The clearance officer role is meant for the
+    assignment of dynamic roles only.
+    """
+    grok.name('waeup.local.ClearanceOfficer')
+    grok.title(u'Clearance Officer')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class LocalStudentsManager(grok.Role):
+    """The local students manager role is meant for the
+    assignment of dynamic roles only.
+    """
+    grok.name('waeup.local.LocalStudentsManager')
+    grok.title(u'Students Manager')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class LocalWorkflowManager(grok.Role):
+    """The local workflow manager role is meant for the
+    assignment of dynamic roles only.
+    """
+    grok.name('waeup.local.LocalWorkflowManager')
+    grok.title(u'Student Workflow Manager')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class UGClearanceOfficer(grok.Role):
+    """The clearance officer role is meant for the
+    assignment of dynamic roles only.
+    """
+    grok.name('waeup.local.UGClearanceOfficer')
+    grok.title(u'UG Clearance Officer')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class PGClearanceOfficer(grok.Role):
+    """The clearance officer role is meant for the
+    assignment of dynamic roles only.
+    """
+    grok.name('waeup.local.PGClearanceOfficer')
+    grok.title(u'PG Clearance Officer')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class CourseAdviser100(grok.Role):
+    """The 100 level course adviser role is meant for the
+    assignment of dynamic roles only.
+    """
+    grok.name('waeup.local.CourseAdviser100')
+    grok.title(u'Course Adviser 100L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class CourseAdviser200(grok.Role):
+    """The course 200 level adviser role is meant for the
+    assignment of dynamic roles only.
+    """
+    grok.name('waeup.local.CourseAdviser200')
+    grok.title(u'Course Adviser 200L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class CourseAdviser300(grok.Role):
+    """The 300 level course adviser role is meant for the
+    assignment of dynamic roles only.
+    """
+    grok.name('waeup.local.CourseAdviser300')
+    grok.title(u'Course Adviser 300L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class CourseAdviser400(grok.Role):
+    """The 400 level course adviser role is meant for the
+    assignment of dynamic roles only.
+    """
+    grok.name('waeup.local.CourseAdviser400')
+    grok.title(u'Course Adviser 400L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class CourseAdviser500(grok.Role):
+    """The 500 level course adviser role is meant for the
+    assignment of dynamic roles only.
+    """
+    grok.name('waeup.local.CourseAdviser500')
+    grok.title(u'Course Adviser 500L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class CourseAdviser600(grok.Role):
+    """The 600 level course adviser role is meant for the
+    assignment of dynamic roles only.
+    """
+    grok.name('waeup.local.CourseAdviser600')
+    grok.title(u'Course Adviser 600L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class CourseAdviser700(grok.Role):
+    """The 700 level course adviser role is meant for the
+    assignment of dynamic roles only.
+    """
+    grok.name('waeup.local.CourseAdviser700')
+    grok.title(u'Course Adviser 700L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class CourseAdviser800(grok.Role):
+    """The 800 level course adviser role is meant for the
+    assignment of dynamic roles only.
+    """
+    grok.name('waeup.local.CourseAdviser800')
+    grok.title(u'Course Adviser 800L')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class Lecturer(grok.Role):
+    """The lecturer role is meant for the
+    assignment of dynamic roles only.
+    """
+    grok.name('waeup.local.Lecturer')
+    grok.title(u'Lecturer')
+    grok.permissions('waeup.showStudents',
+                     'waeup.editScores',
+                     'waeup.viewAcademics',
+                     'waeup.exportData')
+
+class Owner(grok.Role):
+    grok.name('waeup.local.Owner')
+    grok.title(u'Owner')
+    grok.permissions('waeup.editUser')
+
+# Site Roles
+class AcademicsOfficer(grok.Role):
+    grok.name('waeup.AcademicsOfficer')
+    grok.title(u'Academics Officer (view only)')
+    grok.permissions('waeup.viewAcademics')
+
+class AcademicsManager(grok.Role):
+    grok.name('waeup.AcademicsManager')
+    grok.title(u'Academics Manager')
+    grok.permissions('waeup.viewAcademics',
+                     'waeup.manageAcademics')
+
+class ACManager(grok.Role):
+    grok.name('waeup.ACManager')
+    grok.title(u'Access Code Manager')
+    grok.permissions('waeup.manageACBatches')
+
+class DataCenterManager(grok.Role):
+    grok.name('waeup.DataCenterManager')
+    grok.title(u'Datacenter Manager')
+    grok.permissions('waeup.manageDataCenter')
+
+class ImportManager(grok.Role):
+    grok.name('waeup.ImportManager')
+    grok.title(u'Import Manager')
+    grok.permissions('waeup.manageDataCenter',
+                     'waeup.importData')
+
+class ExportManager(grok.Role):
+    grok.name('waeup.ExportManager')
+    grok.title(u'Export Manager')
+    grok.permissions('waeup.manageDataCenter',
+                     'waeup.exportData')
+
+class BursaryOfficer(grok.Role):
+    grok.name('waeup.BursaryOfficer')
+    grok.title(u'Bursary Officer')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportBursaryData')
+
+class UsersManager(grok.Role):
+    grok.name('waeup.UsersManager')
+    grok.title(u'Users Manager')
+    grok.permissions('waeup.manageUsers',
+                     'waeup.editUser')
+
+class WorkflowManager(grok.Role):
+    grok.name('waeup.WorkflowManager')
+    grok.title(u'Workflow Manager')
+    grok.permissions('waeup.triggerTransition')
+
+class PortalManager(grok.Role):
+    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.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.editScores',
+                     'waeup.triggerTransition',
+                     'waeup.viewStudentsContainer','waeup.viewStudentsTab',
+                     'waeup.handleAccommodation',
+                     'waeup.viewHostels', 'waeup.manageHostels',
+                     'waeup.editUser',
+                     'waeup.loginAsStudent',
+                     'waeup.manageReports',
+                     'waeup.manageJobs',
+                     )
+
+class CCOfficer(grok.Role):
+    """This is basically a copy of the the PortalManager class. We exclude some
+    'dangerous' permissions by commenting them out.
+    """
+    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.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.editScores',
+                     #'waeup.triggerTransition',
+                     'waeup.viewStudentsContainer','waeup.viewStudentsTab',
+                     'waeup.handleAccommodation',
+                     'waeup.viewHostels', 'waeup.manageHostels',
+                     #'waeup.editUser',
+                     #'waeup.loginAsStudent',
+                     '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_all_users():
+    """Get a list of dictionaries.
+    """
+    users = sorted(grok.getSite()['users'].items(), key=lambda x: x[1].title)
+    for key, val in users:
+        yield(dict(name=key, val=val))
+
+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/tags/0.2/src/waeup/kofa/permissions.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/permissions.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/permissions.txt	(revision 10886)
@@ -0,0 +1,113 @@
+Kofa 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()))
+    22
+
+    >>> len(list(get_waeup_roles(also_local=True)))
+    42
+
+
+: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.ExportManager',
+     u'waeup.ImportManager',
+     u'waeup.PortalManager',
+     u'waeup.Student',
+     u'waeup.StudentImpersonator',
+     u'waeup.StudentsClearanceOfficer',
+     u'waeup.StudentsCourseAdviser',
+     u'waeup.StudentsManager',
+     u'waeup.StudentsOfficer',
+     u'waeup.TranscriptOfficer',
+     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/tags/0.2/src/waeup/kofa/reports.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/reports.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/reports.py	(revision 10886)
@@ -0,0 +1,385 @@
+## $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():
+        """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 manageReportsPermission(grok.Permission):
+    """A permission to manage reports.
+    """
+    grok.name('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):
+        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.
+        """
+        site = grok.getSite()
+        manager = getUtility(IJobManager)
+        job = AsyncReportJob(site, generator_name, args=args, kw=kw)
+        job_id = manager.put(job)
+        # 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/tags/0.2/src/waeup/kofa/schema/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/schema/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/schema/__init__.py	(revision 10886)
@@ -0,0 +1,3 @@
+from waeup.kofa.schema.field import *
+
+__all__ = tuple(name for name in globals() if not name.startswith('_'))
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/schema/field.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/schema/field.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/schema/field.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/schema/interfaces.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/schema/interfaces.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/schema/interfaces.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/schema/tests/test_fields.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/schema/tests/test_fields.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/schema/tests/test_fields.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/schema/tests/test_textlinechoice.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/schema/tests/test_textlinechoice.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/schema/tests/test_textlinechoice.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/schoolgrades.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/schoolgrades.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/schoolgrades.py	(revision 10886)
@@ -0,0 +1,96 @@
+## $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
+    )
+
+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 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/tags/0.2/src/waeup/kofa/smtp.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/smtp.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/smtp.py	(revision 10886)
@@ -0,0 +1,168 @@
+## $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
+##
+"""
+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
+
+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, then 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.
+
+    .. warn:: 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):
+    """Send mail.
+    """
+    # 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)
+    body["From"] = body["Cc"] = encode_address(from_addr, from_name)
+    body["To"] = body_to.strip(', ')
+    body["Subject"] = encode_header_item(subject)
+
+    mailer = getUtility(IMailService)
+    rcpt_addrs += [from_addr]
+    result = mailer().send(from_addr, rcpt_addrs, body.as_string())
+    return result
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/sourcefactory.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/sourcefactory.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/sourcefactory.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/startup.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/startup.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/startup.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/__init__.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/accommodation.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/accommodation.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/accommodation.py	(revision 10886)
@@ -0,0 +1,106 @@
+## $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
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.kofa.interfaces import academic_sessions_vocab, IKofaUtils
+from waeup.kofa.students.interfaces import (
+    IStudentAccommodation, IStudentNavigation, IBedTicket, IStudentsUtils)
+from waeup.kofa.utils.helpers import attrs_to_fields
+
+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')
+        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)
+
+    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/tags/0.2/src/waeup/kofa/students/authentication.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/authentication.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/authentication.py	(revision 10886)
@@ -0,0 +1,304 @@
+## $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
+
+    @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 or the temporary password set by officers.
+
+        We additionally check if student account has been suspended.
+        """
+        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)
+        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/tags/0.2/src/waeup/kofa/students/batching.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/batching.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/batching.py	(revision 10886)
@@ -0,0 +1,865 @@
+## $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)
+from waeup.kofa.interfaces import IKofaUtils
+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.students.workflow import  (
+    IMPORTABLE_STATES, IMPORTABLE_TRANSITIONS,
+    FORBIDDEN_POSTGRAD_TRANS, FORBIDDEN_POSTGRAD_STATES)
+from waeup.kofa.utils.batching import BatchProcessor
+
+class StudentProcessor(BatchProcessor):
+    """A batch processor for IStudent objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'studentprocessor'
+    grok.name(util_name)
+
+    name = u'Student Processor'
+    iface = IStudent
+    iface_byregnumber = IStudentUpdateByRegNo
+    iface_bymatricnumber = IStudentUpdateByMatricNo
+
+    location_fields = []
+    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)
+        history = IObjectHistory(obj)
+        history.addMessage(_('Student record created'))
+        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
+        # XXX: Take DELETION_MARKER into consideration
+        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
+                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))
+        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)
+        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 = []
+
+    #: header fields additionally required
+    additional_headers = []
+
+    @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_headers:
+            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):
+    """A batch processor for IStudentStudyCourse objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'studycourseupdater'
+    grok.name(util_name)
+
+    name = u'StudentStudyCourse Processor (update only)'
+    iface = IStudentStudyCourse
+    iface_transfer = IStudentStudyCourseTransfer
+    factory_name = 'waeup.StudentStudyCourse'
+
+    location_fields = []
+    additional_fields = []
+
+    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.
+        """
+        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):
+    """A batch processor for IStudentStudyLevel objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'studylevelprocessor'
+    grok.name(util_name)
+
+    name = u'StudentStudyLevel Processor'
+    iface = IStudentStudyLevel
+    factory_name = 'waeup.StudentStudyLevel'
+
+    location_fields = []
+
+    additional_fields = ['level']
+    additional_headers = ['level']
+
+    @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
+        return studycourse.get(row['level'])
+
+    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)
+        obj.level = int(row['level'])
+        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):
+        parent = self.getParent(row, site)
+        obj.level = int(row['level'])
+        parent[row['level']] = obj
+        return
+
+    def checkConversion(self, row, mode='ignore'):
+        """Validates all values in row.
+        """
+        errs, inv_errs, conv_dict = super(
+            StudentStudyLevelProcessor, self).checkConversion(row, mode=mode)
+
+        # We have to check if level is a valid integer.
+        # This is not done by the converter.
+        try:
+            level = int(row['level'])
+            if level not in range(0,1000,10) + [999]:
+                errs.append(('level','no valid integer'))
+        except ValueError:
+            errs.append(('level','no integer'))
+        return errs, inv_errs, conv_dict
+
+class CourseTicketProcessor(StudentProcessorBase):
+    """A batch processor for ICourseTicket objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'courseticketprocessor'
+    grok.name(util_name)
+
+    name = u'CourseTicket Processor'
+    iface = ICourseTicketImport
+    factory_name = 'waeup.CourseTicket'
+
+    location_fields = []
+    additional_fields = ['level', 'code']
+    additional_headers = ['level', 'code']
+
+    @property
+    def available_fields(self):
+        fields = [
+            'student_id','reg_number','matric_number',
+            'mandatory', 'score', 'carry_over', 'automatic',
+            'level_session'
+            ] + 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(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
+        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
+        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 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','does not exist'))
+        return errs, inv_errs, conv_dict
+
+class StudentOnlinePaymentProcessor(StudentProcessorBase):
+    """A batch processor for IStudentOnlinePayment objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'paymentprocessor'
+    grok.name(util_name)
+
+    name = u'Student Payment Processor'
+    iface = IStudentOnlinePayment
+    factory_name = 'waeup.StudentOnlinePayment'
+
+    location_fields = []
+    additional_fields = ['p_id']
+    additional_headers = []
+
+    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 is None:
+            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 addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        p_id = row['p_id'].strip('#')
+        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 not p_id:
+            timestamp = ("%d" % int(time()*10000))[1:]
+            p_id = "p%s" % timestamp
+            conv_dict['p_id'] = p_id
+            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 length'))
+                return errs, inv_errs, conv_dict
+        return errs, inv_errs, conv_dict
+
+class StudentVerdictProcessor(StudentStudyCourseProcessor):
+    """A special batch processor for verdicts.
+
+    Import verdicts and perform workflow transitions.
+    """
+
+    util_name = 'verdictupdater'
+    grok.name(util_name)
+
+    name = u'Verdict Processor (special processor, update only)'
+    iface = IStudentVerdictUpdate
+    factory_name = 'waeup.StudentStudyCourse'
+
+    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_vedict 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/tags/0.2/src/waeup/kofa/students/browser.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser.py	(revision 10886)
@@ -0,0 +1,3336 @@
+## $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 sys
+import grok
+import pytz
+from urllib import urlencode
+from datetime import datetime
+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 hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from waeup.kofa.accesscodes import (
+    invalidate_accesscode, get_access_code)
+from waeup.kofa.accesscodes.workflow import USED
+from waeup.kofa.browser.layout import (
+    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
+    KofaForm, NullValidator)
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+from waeup.kofa.browser.pages import ContactAdminForm, ExportCSVView, doll_up
+from waeup.kofa.browser.resources import (
+    datepicker, datatable, tabs, warning, toggleall)
+from waeup.kofa.browser.layout import jsaction, action, UtilityView
+from waeup.kofa.browser.interfaces import ICaptchaManager
+from waeup.kofa.hostels.hostel import NOT_OCCUPIED
+from waeup.kofa.interfaces import (
+    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
+    IKofaUtils, IUniversity, IObjectHistory, academic_sessions, ICSVExporter,
+    academic_sessions_vocab, IJobManager, IDataCenter)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.widgets.datewidget import (
+    FriendlyDateWidget, FriendlyDateDisplayWidget,
+    FriendlyDatetimeDisplayWidget)
+from waeup.kofa.mandates.mandate import PasswordMandate
+from waeup.kofa.university.interfaces import (
+    IDepartment, ICertificate, ICourse)
+from waeup.kofa.university.department import (
+    VirtualDepartmentExportJobContainer,)
+from waeup.kofa.university.facultiescontainer import (
+    VirtualFacultiesExportJobContainer, FacultiesContainer)
+from waeup.kofa.university.certificate import (
+    VirtualCertificateExportJobContainer,)
+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, to_timezone, now
+from waeup.kofa.widgets.restwidget import ReSTDisplayWidget
+from waeup.kofa.students.interfaces import (
+    IStudentsContainer, IStudent,
+    IUGStudentClearance,IPGStudentClearance,
+    IStudentPersonal, IStudentPersonalEdit, IStudentBase, IStudentStudyCourse,
+    IStudentStudyCourseTransfer, IStudentStudyCourseTranscript,
+    IStudentAccommodation, IStudentStudyLevel,
+    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
+    IStudentOnlinePayment, IStudentPreviousPayment, IStudentBalancePayment,
+    IBedTicket, IStudentsUtils, IStudentRequestPW, IStudentTranscript
+    )
+from waeup.kofa.students.catalog import search, StudentQueryResultItem
+from waeup.kofa.students.export import EXPORTER_NAMES
+from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
+from waeup.kofa.students.vocabularies import StudyLevelSource
+from waeup.kofa.students.workflow import (CREATED, ADMITTED, PAID,
+    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED,
+    GRADUATED, TRANSCRIPT, FORBIDDEN_POSTGRAD_TRANS)
+
+
+grok.context(IKofaObject) # Make IKofaObject the default context
+
+# 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).'))
+    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['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['automatic'] = translate(str(value.automatic), 'zope',
+            target_language=lang)
+        value_dict['grade'] = value.grade
+        value_dict['weight'] = value.weight
+        semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
+        value_dict['semester'] = semester_dict[
+            value.semester].replace('mester', 'm.')
+        yield value_dict
+
+def clearance_disabled_message(student):
+    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 addCourseTicket(view, course=None):
+    students_utils = getUtility(IStudentsUtils)
+    ticket = createObject(u'waeup.CourseTicket')
+    ticket.automatic = False
+    ticket.carry_over = False
+    max_credits = students_utils.maxCreditsExceeded(view.context, course)
+    if max_credits:
+        view.flash(_(
+            'Total credits exceed ${a}.',
+            mapping = {'a': max_credits}))
+        return False
+    try:
+        view.context.addCourseTicket(ticket, course)
+    except KeyError:
+        view.flash(_('The ticket exists.'))
+        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 = _('Student Section')
+    search_button = _('Find student(s)')
+    pnav = 4
+
+    def update(self, *args, **kw):
+        datatable.need()
+        form = self.request.form
+        self.hitlist = []
+        if form.get('searchtype', None) == 'suspended':
+            self.searchtype = form['searchtype']
+            self.searchterm = None
+        elif form.get('searchtype', None) == 'transcript':
+            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'))
+            return
+        if self.searchtype == 'current_session':
+            try:
+                self.searchterm = int(self.searchterm)
+            except ValueError:
+                self.flash(_('Only year dates allowed (e.g. 2011).'))
+                return
+        self.hitlist = search(query=self.searchterm,
+            searchtype=self.searchtype, view=self)
+        if not self.hitlist:
+            self.flash(_('No student found.'))
+        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 student section')
+    search_button = _('Find student(s)')
+    remove_button = _('Remove selected')
+
+    def update(self, *args, **kw):
+        datatable.need()
+        toggleall.need()
+        warning.need()
+        form = self.request.form
+        self.hitlist = []
+        if form.get('searchtype', None) == 'suspended':
+            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'))
+            return
+        if self.searchtype == 'current_session':
+            try:
+                self.searchterm = int(self.searchterm)
+            except ValueError:
+                self.flash('Only year dates allowed (e.g. 2011).')
+                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.'))
+            if 'remove' in form:
+                self.flash(_('No item selected.'))
+            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 record'), 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
+
+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 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')
+    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')
+
+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 ContactStudentForm(ContactAdminForm):
+    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(ContactStudentForm, 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.'))
+        return
+
+class ExportPDFAdmissionSlipPage(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',)
+
+    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)
+        return students_utils.renderPDFAdmissionLetter(self,
+            self.context.student, omit_fields=self.omit_fields)
+
+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):
+        datepicker.need() # Enable jQuery datepicker in date fields.
+        tabs.need()
+        self.tab1 = self.tab2 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        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))
+                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 manage student base data
+    """
+    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 StudentActivatePage(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 StudentDeactivatePage(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).omit('clearance_locked')
+        else:
+            form_fields = grok.AutoFields(
+                IUGStudentClearance).omit('clearance_locked')
+        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 ExportPDFClearanceSlipPage(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')
+
+    @property
+    def form_fields(self):
+        if self.context.is_postgrad:
+            form_fields = grok.AutoFields(
+                IPGStudentClearance).omit('clearance_locked')
+        else:
+            form_fields = grok.AutoFields(
+                IUGStudentClearance).omit('clearance_locked')
+        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')
+    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
+
+    def update(self):
+        datepicker.need() # Enable jQuery datepicker in date fields.
+        tabs.need()
+        self.tab1 = self.tab2 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        return super(StudentClearanceManageFormPage, self).update()
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+class StudentClearPage(UtilityView, grok.View):
+    """ Clear student by clearance officer
+    """
+    grok.context(IStudent)
+    grok.name('clear')
+    grok.require('waeup.clearStudent')
+
+    def update(self):
+        if clearance_disabled_message(self.context):
+            self.flash(clearance_disabled_message(self.context))
+            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.'))
+        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):
+        if clearance_disabled_message(self.context):
+            self.flash(clearance_disabled_message(self.context))
+            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)
+        elif self.context.state == REQUESTED:
+            IWorkflowInfo(self.context).fireTransition('reset3')
+            message = _('Clearance request has been rejected.')
+            self.flash(message)
+        else:
+            self.flash(_('Student is in wrong state.'))
+            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):
+        if self.context.certificate is not None:
+            return self.context.certificate.__parent__.__parent__
+        return
+
+    @property
+    def faculty(self):
+        if self.context.certificate is not None:
+            return self.context.certificate.__parent__.__parent__.__parent__
+        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:
+            emit_lock_message(self)
+            return
+        super(StudyCourseManageFormPage, self).update()
+        tabs.need()
+        self.tab1 = self.tab2 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        warning.need()
+        datatable.need()
+        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.'))
+            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'))
+    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:
+            self.flash(_('You must select a session for the level.'))
+            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
+            return
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = int(level_code)
+        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.'))
+        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.'))
+            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 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()
+        tabs.need()
+        self.tab1 = self.tab2 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        if not self.context.state == GRADUATED:
+            self.flash(_("Wrong state"))
+            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 self.context.transcript_comment is not None:
+            self.correspondence = self.context.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.'))
+                return
+            if code.state == USED:
+                self.flash(_('Activation code has already been used.'))
+                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.'))
+                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 = self.context.transcript_comment
+        if old_transcript_comment == None:
+            old_transcript_comment = ''
+        self.context.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 StudentTranscriptRequestProcessFormPage(KofaEditFormPage):
+    """ Page to process transcript requests
+    """
+    grok.context(IStudent)
+    grok.name('process_transcript_request')
+    grok.require('waeup.viewTranscript')
+    grok.template('transcriptprocess')
+    form_fields = grok.AutoFields(IStudentTranscript)
+    label = _('Process transcript request')
+    buttonname = _('Save comment and mark as processed')
+    pnav = 4
+
+    def update(self, SUBMIT=None):
+        super(StudentTranscriptRequestProcessFormPage, self).update()
+        tabs.need()
+        self.tab1 = self.tab2 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        if self.context.state != TRANSCRIPT:
+            self.flash(_('Student is in wrong state.'))
+            self.redirect(self.url(self.context))
+            return
+        if self.context.transcript_comment is not None:
+            self.correspondence = self.context.transcript_comment.replace(
+                '\n', '<br>')
+        else:
+            self.correspondence = ''
+        if SUBMIT is None:
+            return
+        IWorkflowInfo(self.context).fireTransition('process_transcript')
+        self.flash(_('Transcript request processed.'))
+        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 = self.context.transcript_comment
+        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>'))
+        subject = _('Transcript processed')
+        args = {'subject':subject, 'body':comment}
+        self.redirect(self.url(self.context) +
+            '/contactstudent?%s' % urlencode(args))
+        return
+
+class StudentTranscriptRequestManageFormPage(KofaEditFormPage):
+    """ Page to edit personal data by student
+    """
+    grok.context(IStudent)
+    grok.name('manage_transcript_request')
+    grok.require('waeup.manageStudent')
+    form_fields = grok.AutoFields(IStudentTranscript)
+    label = _('Manage transcript request')
+    pnav = 4
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+class StudyCourseTranscriptPage(KofaDisplayFormPage):
+    """ Page to display the student's transcript.
+    """
+    grok.context(IStudentStudyCourseTranscript)
+    grok.name('transcript')
+    grok.require('waeup.viewTranscript')
+    grok.template('transcript')
+    pnav = 4
+
+    def update(self):
+        if not self.context.student.transcript_enabled:
+            self.flash(_('You are not allowed to view the transcript.'))
+            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(
+            [(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
+        lang = self.request.cookies.get('kofa.language')
+        return _('${a}: Transcript Data', mapping = {
+            'a':self.context.student.display_fullname})
+
+class ExportPDFTranscriptPage(UtilityView, grok.View):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(IStudentStudyCourse)
+    grok.name('transcript.pdf')
+    grok.require('waeup.viewTranscript')
+    form_fields = grok.AutoFields(IStudentStudyCourseTranscript)
+    prefix = 'form'
+    omit_fields = (
+        'department', 'faculty', 'current_mode', 'entry_session', 'certificate',
+        'password', 'suspended', 'phone', 'email',
+        'adm_code', 'suspended_comment')
+
+    def update(self):
+        if not self.context.student.transcript_enabled:
+            self.flash(_('You are not allowed to download the transcript.'))
+            self.redirect(self.url(self.context))
+            return
+        super(ExportPDFTranscriptPage, self).update()
+        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
+        self.level_dict = level_dict(self.context)
+        self.session_dict = dict(
+            [(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):
+        return (_('CERTIFIED TRUE COPY'),)
+
+    def _signatures(self):
+        return None
+
+    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, 'score', 1.5),
+                         (Grade, 'grade', 1.5),
+                         ]
+
+        return 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(),
+            )
+
+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
+
+    def update(self):
+        super(StudentTransferFormPage, self).update()
+        warning.need()
+        return
+
+    @jsaction(_('Transfer'))
+    def transferStudent(self, **data):
+        error = self.context.transfer(**data)
+        if error == -1:
+            self.flash(_('Current level does not match certificate levels.'))
+        elif error == -2:
+            self.flash(_('Former study course record incomplete.'))
+        elif error == -3:
+            self.flash(_('Maximum number of transfers exceeded.'))
+        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):
+        warning.need()
+        if not self.context.has_key('studycourse_1'):
+            self.flash(_('No previous transfer.'))
+            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)
+    form_fields[
+        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    grok.template('studylevelpage')
+    pnav = 4
+
+    def update(self):
+        super(StudyLevelDisplayFormPage, self).update()
+        datatable.need()
+        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}: Study Level ${b}', mapping = {
+            'a':self.context.student.display_fullname,
+            'b':level_title})
+
+class ExportPDFCourseRegistrationSlipPage(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)
+    form_fields[
+        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    prefix = 'form'
+    omit_fields = (
+        'password', 'suspended', 'phone', 'date_of_birth',
+        'adm_code', 'sex', 'suspended_comment')
+
+    @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 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)
+        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 = []
+        contenttitle = []
+        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
+            )
+
+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')
+    pnav = 4
+    taboneactions = [_('Save'),_('Cancel')]
+    tabtwoactions = [_('Add course ticket'),
+        _('Remove selected tickets'),_('Cancel')]
+
+    def update(self, ADD=None, course=None):
+        if not self.context.__parent__.is_current:
+            emit_lock_message(self)
+            return
+        super(StudyLevelManageFormPage, self).update()
+        tabs.need()
+        self.tab1 = self.tab2 = ''
+        qs = self.request.get('QUERY_STRING', '')
+        if not qs:
+            qs = 'tab1'
+        setattr(self, qs, 'active')
+        warning.need()
+        datatable.need()
+        if ADD is not None:
+            if not course:
+                self.flash(_('No valid course code entered.'))
+                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.'))
+            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 study level ${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.'))
+            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 at %s' %
+                (', '.join(deleted), self.context.level))
+        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
+        return
+
+class ValidateCoursesPage(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.__parent__.current_level) != self.context.__name__:
+            self.flash(_('This level does not correspond current level.'))
+        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.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class RejectCoursesPage(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 level does not correspond current level.'))
+            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 request has been rejected:')
+            self.flash(message)
+        else:
+            self.flash(_('Student is in the wrong state.'))
+            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 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:
+            emit_lock_message(self)
+            return
+        super(CourseTicketAddFormPage, self).update()
+        return
+
+    @action(_('Add course ticket'))
+    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)
+    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)
+    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
+    pnav = 4
+    grok.template('courseticketmanagepage')
+
+    @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})
+
+    def update(self):
+        super(PaymentsManageFormPage, self).update()
+        datatable.need()
+        warning.need()
+        return
+
+    @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.'))
+            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_category')
+    label = _('Add online payment')
+    pnav = 4
+
+    @property
+    def selectable_categories(self):
+        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
+        return sorted(categories.items())
+
+    @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__
+        if p_category == 'bed_allocation' and student[
+            'studycourse'].current_session != grok.getSite()[
+            'hostels'].accommodation_session:
+                self.flash(
+                    _('Your current session does not match ' + \
+                    'accommodation session.'))
+                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.'))
+                return
+        students_utils = getUtility(IStudentsUtils)
+        error, payment = students_utils.setPaymentDetails(
+            p_category, student, previous_session, previous_level)
+        if error is not None:
+            self.flash(error)
+            return
+        self.context[payment.p_id] = payment
+        self.flash(_('Payment ticket created.'))
+        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."))
+            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)
+        if error is not None:
+            self.flash(error)
+            return
+        self.context[payment.p_id] = payment
+        self.flash(_('Payment ticket created.'))
+        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 ticket for balance sessions
+    """
+    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)
+            return
+        self.context[payment.p_id] = payment
+        self.flash(_('Payment ticket created.'))
+        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')
+    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 OnlinePaymentApprovePage(UtilityView, grok.View):
+    """ Callback view
+    """
+    grok.context(IStudentOnlinePayment)
+    grok.name('approve')
+    grok.require('waeup.managePortal')
+
+    def update(self):
+        success, 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)
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
+    """ Approval view for students.
+
+    This view is used for browser tests only and
+    must be neutralized in custom pages!
+    """
+
+    grok.name('fake_approve')
+    grok.require('waeup.payStudent')
+
+class ExportPDFPaymentSlipPage(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')
+    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')
+
+    @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 AccommodationManageFormPage(KofaEditFormPage):
+    """ Page to manage bed tickets.
+
+    This manage form page is for both students and students officers.
+    """
+    grok.context(IStudentAccommodation)
+    grok.name('index')
+    grok.require('waeup.handleAccommodation')
+    form_fields = grok.AutoFields(IStudentAccommodation)
+    grok.template('accommodationmanagepage')
+    pnav = 4
+    officers_only_actions = [_('Remove selected')]
+
+    @property
+    def label(self):
+        return _('${a}: Accommodation',
+            mapping = {'a':self.context.__parent__.display_fullname})
+
+    def update(self):
+        super(AccommodationManageFormPage, self).update()
+        datatable.need()
+        warning.need()
+        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.'))
+            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.'))
+            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
+
+    @property
+    def selected_actions(self):
+        if getattr(self.request.principal, 'user_type', None) == 'student':
+            return [action for action in self.actions
+                    if not action.label in self.officers_only_actions]
+        return self.actions
+
+class BedTicketAddPage(KofaPage):
+    """ Page to add an online payment 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
+
+    def update(self, SUBMIT=None):
+        student = self.context.student
+        students_utils = getUtility(IStudentsUtils)
+        acc_details  = students_utils.getAccommodationDetails(student)
+        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")
+                self.flash(_("Outside booking period: ${a} - ${b}",
+                    mapping = {'a': startdate, 'b': enddate}))
+            else:
+                self.flash(_("Outside booking period."))
+            self.redirect(self.url(self.context))
+            return
+        if not acc_details:
+            self.flash(_("Your data are incomplete."))
+            self.redirect(self.url(self.context))
+            return
+        if not student.state in acc_details['allowed_states']:
+            self.flash(_("You are in the wrong registration state."))
+            self.redirect(self.url(self.context))
+            return
+        if student['studycourse'].current_session != acc_details[
+            'booking_session']:
+            self.flash(
+                _('Your current session does not match accommodation session.'))
+            self.redirect(self.url(self.context))
+            return
+        if str(acc_details['booking_session']) in self.context.keys():
+            self.flash(
+                _('You already booked a bed space in current ' \
+                    + 'accommodation session.'))
+            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.'))
+                return
+        # Search and book bed
+        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
+        entries = cat.searchResults(
+            owner=(student.student_id,student.student_id))
+        if len(entries):
+            # If bed space has been manually allocated use this bed
+            bed = [entry for entry in entries][0]
+            # Safety belt for paranoids: Does this bed really exist on portal?
+            # XXX: Can be remove if nobody complains.
+            if bed.__parent__.__parent__ is None:
+                self.flash(_('System error: Please contact the adminsitrator.'))
+                self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
+                return
+        else:
+            # 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]
+            if available_beds:
+                students_utils = getUtility(IStudentsUtils)
+                bed = students_utils.selectBed(available_beds)
+                # Safety belt for paranoids: Does this bed really exist in portal?
+                # XXX: Can be remove if nobody complains.
+                if bed.__parent__.__parent__ is None:
+                    self.flash(_('System error: Please contact the adminsitrator.'))
+                    self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
+                    return
+                bed.bookBed(student.student_id)
+            else:
+                self.flash(_('There is no free bed in your category ${a}.',
+                    mapping = {'a':acc_details['bt']}))
+                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.'))
+                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
+                if not invalidate_accesscode(
+                    pin,comment,self.context.student.student_id):
+                    self.flash(_('You are not the owner of this access code.'))
+                    return
+        # Create bed ticket
+        bedticket = createObject(u'waeup.BedTicket')
+        if self.with_ac:
+            bedticket.booking_code = pin
+        bedticket.booking_session = acc_details['booking_session']
+        bedticket.bed_type = 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.handleAccommodation')
+    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 ExportPDFBedTicketSlipPage(UtilityView, grok.View):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(IBedTicket)
+    grok.name('bed_allocation_slip.pdf')
+    grok.require('waeup.handleAccommodation')
+    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')
+
+    @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)
+        return students_utils.renderPDF(
+            self, 'bed_allocation_slip.pdf',
+            self.context.student, studentview,
+            omit_fields=self.omit_fields)
+
+class BedTicketRelocationPage(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):
+        student = self.context.student
+        students_utils = getUtility(IStudentsUtils)
+        acc_details  = students_utils.getAccommodationDetails(student)
+        if self.context.bed != None and \
+              'reserved' in self.context.bed.bed_type:
+            self.flash(_("Students in reserved beds can't be relocated."))
+            self.redirect(self.url(self.context))
+            return
+        if acc_details['bt'] == self.context.bed_type and \
+                self.context.bed != None and \
+                self.context.bed.bed_type == self.context.bed_type:
+            self.flash(_("Student can't be relocated."))
+            self.redirect(self.url(self.context))
+            return
+        # 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.context.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]
+            if available_beds:
+                students_utils = getUtility(IStudentsUtils)
+                new_bed = students_utils.selectBed(available_beds)
+                new_bed.bookBed(student.student_id)
+            else:
+                self.flash(_('There is no free bed in your category ${a}.',
+                    mapping = {'a':acc_details['bt']}))
+                self.redirect(self.url(self.context))
+                return
+        # Release old bed if exists
+        if self.context.bed != None:
+            self.context.bed.owner = NOT_OCCUPIED
+            notify(grok.ObjectModifiedEvent(self.context.bed))
+        # Alocate new bed
+        self.context.bed_type = acc_details['bt']
+        self.context.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.context.bed_coordinates = translate(
+            bc, 'waeup.kofa',target_language=portal_language)
+        self.context.writeLogMessage(self, 'relocated: %s' % new_bed.bed_id)
+        self.flash(_('Student relocated: ${a}',
+            mapping = {'a':self.context.display_coordinates}))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class StudentHistoryPage(KofaPage):
+    """ Page to display student clearance data
+    """
+    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')
+    label = _('Edit base data')
+    pnav = 4
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+class StudentChangePasswordPage(KofaEditFormPage):
+    """ View to manage student base data
+    """
+    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)
+                self.context.writeLogMessage(self, 'saved: password')
+                self.flash(_('Password changed.'))
+            else:
+                self.flash( ' '.join(errors))
+        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):
+        PWCHANGE_STATES = getUtility(IStudentsUtils).PWCHANGE_STATES
+        if self.context.student.state not in PWCHANGE_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 self.context.email and self.context.phone:
+            return True
+        return False
+
+    @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"))
+            self.redirect(self.url(self.context))
+            return
+        if not self.portrait_uploaded:
+            self.flash(_("No portrait uploaded."))
+            self.redirect(self.url(self.context, 'change_portrait'))
+            return
+        if not self.all_required_fields_filled:
+            self.flash(_("Not all required fields filled."))
+            self.redirect(self.url(self.context, 'edit_base'))
+            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.'))
+                return
+            if code.state == USED:
+                self.flash(_('Activation code has already been used.'))
+                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.'))
+                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(
+                'clearance_locked', 'clr_code', 'officer_comment')
+        else:
+            form_fields = grok.AutoFields(IUGStudentClearance).omit(
+                'clearance_locked', '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')
+    def requestClearance(self, **data):
+        self.applyData(self.context, **data)
+        if self.dataNotComplete():
+            self.flash(self.dataNotComplete())
+            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!'))
+                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."))
+                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!'))
+            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."))
+            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.'))
+                return
+            # Mark pin as used (this also fires a pin related transition)
+            if code.state == USED:
+                self.flash(_('Activation code has already been used.'))
+                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.'))
+                    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.'))
+            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:
+            emit_lock_message(self)
+            return
+        if self.context.student.state != PAID:
+            emit_lock_message(self)
+            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.'))
+        except RequiredMissing:
+            self.flash(_('Your data are incomplete'))
+        self.redirect(self.url(self.context))
+        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')
+    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
+        'level_session', 'level_verdict')
+    pnav = 4
+
+    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()
+        datatable.need()
+        warning.need()
+        if ADD is not None:
+            if not course:
+                self.flash(_('No valid course code entered.'))
+                return
+            cat = queryUtility(ICatalog, name='courses_catalog')
+            result = cat.searchResults(code=(course, course))
+            if len(result) != 1:
+                self.flash(_('Course not found.'))
+                return
+            course = list(result)[0]
+            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.'))
+            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 _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."))
+            self.redirect(self.url(self.context))
+            return
+        students_utils = getUtility(IStudentsUtils)
+        max_credits = students_utils.maxCredits(self.context)
+        if self.context.total_credits > max_credits:
+            self.flash(_('Maximum credits of ${a} exceeded.',
+                mapping = {'a':max_credits}))
+            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'))
+    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']
+        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.'))
+            return
+        if len(hitlist) != 1:   # Cannot happen but anyway
+            self.flash(_('More than one student found.'))
+            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.'))
+            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.'))
+            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.'))
+            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 registration page for applicants.
+    """
+    grok.name('requestpw')
+    grok.require('waeup.Anonymous')
+    grok.template('requestpw')
+    form_fields = grok.AutoFields(IStudentRequestPW).select(
+        'firstname','number','email')
+    label = _('Request password for first-time login')
+
+    def update(self):
+        # 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 _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','')
+        firstname = data.get('firstname','')
+        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,'firstname',None) is None:
+                self.flash(_('An error occurred.'))
+                return
+            elif student.firstname.lower() != firstname.lower():
+                # Don't tell the truth here. Anonymous must not
+                # know that a record was found and only the firstname
+                # verification failed.
+                self.flash(_('No student record found.'))
+                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.'))
+                return
+            # Store email address but nothing else.
+            student.email = data['email']
+            notify(grok.ObjectModifiedEvent(student))
+        else:
+            # No record found, this is the truth.
+            self.flash(_('No student record found.'))
+            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 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):
+        datatable.need()
+        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.'))
+        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)
+
+    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 EditScoresPage(KofaPage):
+    """Page that filters and lists students.
+    """
+    grok.context(ICourse)
+    grok.require('waeup.editScores')
+    grok.name('edit_scores')
+    grok.template('editscorespage')
+    pnav = 1
+
+    def label(self):
+        session = academic_sessions_vocab.getTerm(
+            self.current_academic_session).title
+        return '%s tickets in academic session %s' % (
+            self.context.code, session)
+
+    def _searchCatalog(self, session):
+        cat = queryUtility(ICatalog, name='coursetickets_catalog')
+        coursetickets = cat.searchResults(
+            session=(session, session),
+            code=(self.context.code, self.context.code)
+            )
+        return list(coursetickets)
+
+    def update(self,  *args, **kw):
+        form = self.request.form
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.current_academic_session = grok.getSite()[
+            'configuration'].current_academic_session
+        if self.context.__parent__.__parent__.score_editing_disabled:
+            self.flash(_('Score editing disabled.'))
+            self.redirect(self.url(self.context))
+            return
+        if not self.current_academic_session:
+            self.flash(_('Current academic session not set.'))
+            self.redirect(self.url(self.context))
+            return
+        datatable.need()
+        self.tickets = self._searchCatalog(self.current_academic_session)
+        editable_tickets = [
+            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
+        if not self.tickets:
+            self.flash(_('No student found.'))
+            self.redirect(self.url(self.context))
+            return
+        if 'UPDATE' in form:
+            tno = 0
+            error = ''
+            if not editable_tickets:
+                return
+            scores = form['scores']
+            if isinstance(scores, basestring):
+                scores = [scores]
+            for ticket in editable_tickets:
+                score = ticket.score
+                if scores[tno] == '':
+                    score = None
+                else:
+                    try:
+                        score = int(scores[tno])
+                    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))
+                    #notify(grok.ObjectModifiedEvent(ticket))
+                tno += 1
+            if error:
+                self.flash(_('Error: Score(s) of %s have not be updated. '
+                  'Only integers are allowed.' % error.strip(', ')))
+        return
+
+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
+
+    def update(self, CREATE=None, DISCARD=None, job_id=None):
+        if CREATE:
+            self.redirect(self.url('@@exportconfig'))
+            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.name('exportconfig')
+    grok.require('waeup.showStudents')
+    grok.template('exportconfig')
+    label = _('Configure student data export')
+    pnav = 1
+    redirect_target = ''
+
+    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 _set_mode_values(self):
+        utils = getUtility(IKofaUtils)
+        self.modes = sorted([(value, key) for key, value in
+                      utils.STUDY_MODES_DICT.items()])
+        self.modes +=[(_('All Modes'), 'all')]
+        return
+
+    def _set_exporter_values(self):
+        # We provide all student exporters, nothing else, yet.
+        # Bursary or Department Officers don't have the general exportData 
+        # permission and are only allowed to export bursary or payments
+        # overview data respectively. This is the only place where
+        # 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 += [('Student Payments Overview', 'paymentsoverview')]
+            self.exporters = exporters
+            return
+        for name in EXPORTER_NAMES:
+            util = getUtility(ICSVExporter, name=name)
+            exporters.append((util.title, name),)
+        self.exporters = exporters
+        return
+
+    @property
+    def depcode(self):
+        return None
+
+    @property
+    def certcode(self):
+        return None
+
+    def update(self, START=None, session=None, level=None, mode=None,
+               exporter=None):
+        self._set_session_values()
+        self._set_level_values()
+        self._set_mode_values()
+        self._set_exporter_values()
+        if START is None:
+            return
+        if session == 'all':
+            session=None
+        if level == 'all':
+            level = None
+        if mode == 'all':
+            mode = None
+        if (mode, level, session,
+            self.depcode, self.certcode) == (None, None, None, None, None):
+            # Export all students including those without certificate
+            job_id = self.context.start_export_job(exporter,
+                                          self.request.principal.id)
+        else:
+            job_id = self.context.start_export_job(exporter,
+                                          self.request.principal.id,
+                                          current_session=session,
+                                          current_level=level,
+                                          current_mode=mode,
+                                          depcode=self.depcode,
+                                          certcode=self.certcode)
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.logger.info(
+            '%s - exported: %s (%s, %s, %s, %s, %s), job_id=%s'
+            % (ob_class, exporter, session, level, mode, self.depcode,
+            self.certcode, 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.context(IDataCenter)
+    redirect_target = '@@export'
+
+class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
+    """Page that configures a students export job in facultiescontainer.
+
+    """
+    grok.context(VirtualFacultiesExportJobContainer)
+
+class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
+    """Page that configures a students export job in departments.
+
+    """
+    grok.context(VirtualDepartmentExportJobContainer)
+
+    @property
+    def depcode(self):
+        return self.context.__parent__.code
+
+class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
+    """Page that configures a students export job for certificates.
+
+    """
+    grok.context(VirtualCertificateExportJobContainer)
+    grok.template('exportconfig_certificate')
+
+    @property
+    def certcode(self):
+        return self.context.__parent__.code
+
+class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
+    """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')
+
+    def _set_exporter_values(self):
+        # We provide only two exporters.
+        exporters = []
+        for name in ('students', 'coursetickets'):
+            util = getUtility(ICSVExporter, name=name)
+            exporters.append((util.title, name),)
+        self.exporters = exporters
+
+    def update(self, START=None, session=None, level=None, mode=None,
+               exporter=None):
+        self._set_session_values()
+        self._set_level_values()
+        self._set_mode_values()
+        self._set_exporter_values()
+        if START is None:
+            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/tags/0.2/src/waeup/kofa/students/browser_templates/accommodationmanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/accommodationmanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/accommodationmanagepage.pt	(revision 10886)
@@ -0,0 +1,36 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <table class="display 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 Category</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 cl/__name__">
+        <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>
+
+  <div tal:condition="view/availableActions">
+    <span tal:repeat="action view/selected_actions"
+          tal:omit-tag="">
+      <input tal:replace="structure action/render"/>
+    </span>
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/basemanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/basemanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/basemanagepage.pt	(revision 10886)
@@ -0,0 +1,48 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data"
+      autocomplete="off">
+
+  <ul class="tabs" data-tabs="tabs">
+    <li tal:attributes="class view/tab1"><a href="#tab-1">
+    <span i18n:translate="">Base Data</span></a></li>
+    <li tal:attributes="class view/tab2"><a href="#tab-2">
+    <span i18n:translate="">Passport Picture</span></a></li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+      <table class="form-table">
+        <tbody>
+          <tal:widgets content="structure provider:widgets" />
+          <tr>
+            <td class="fieldname" i18n:translate="">Password:</td>
+            <td>
+              <input name="password" type="password"  />
+            </td>
+          </tr>
+          <tr>
+            <td class="fieldname" i18n:translate="">Retype Password:</td>
+            <td>
+              <input name="control_password" 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="tab-2"  tal:attributes="class view/tab2">
+      <table class="form-table">
+        <tbody>
+          <tal:files content="structure provider:files" />
+        </tbody>
+      </table>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/basepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/basepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/basepage.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/browser_templates/change_password.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/change_password.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/change_password.pt	(revision 10886)
@@ -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" type="password"  />
+        </td>
+      </tr>
+      <tr>
+        <td class="fieldname" i18n:translate="">Retype Password:</td>
+        <td>
+          <input name="change_password_repeat" type="password" />
+        </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/tags/0.2/src/waeup/kofa/students/browser_templates/clearanceeditpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/clearanceeditpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/clearanceeditpage.pt	(revision 10886)
@@ -0,0 +1,33 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <ul class="tabs" data-tabs="tabs">
+    <li tal:attributes="class view/tab1"><a href="#tab-1">
+    <span i18n:translate="">Clearance Data</span></a></li>
+    <li tal:attributes="class view/tab2"><a href="#tab-2">
+    <span i18n:translate="">Scans</span></a></li>
+  </ul>
+  <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+      <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>
+    </div>
+
+    <div id="tab-2" tal:attributes="class view/tab2">
+      <table class="form-table">
+        <tbody>
+          <tal:files content="structure provider:files" />
+        </tbody>
+      </table>
+    </div>
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/containermanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/containermanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/containermanagepage.pt	(revision 10886)
@@ -0,0 +1,68 @@
+<form method="POST" i18n:domain="waeup.kofa">
+<br />
+  <input class="btn primary" type="submit" name="search"
+    tal:attributes="value view/search_button" />
+
+  <select name="searchtype">
+    <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>
+  </select>
+
+  <input type="text" name="searchterm" />
+
+  <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" />
+
+    <input type="checkbox" onClick="toggle(this, 'entries')" />
+    <span  i18n:translate="">Select all</span>
+    <br /><br />
+
+    <table class="display 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" class="gradeC">
+    	  <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="submit" name="remove"
+           tal:attributes="value view/remove_button" class="btn"
+           onclick="return confirmPost('Are you sure?')"/>
+
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/containerpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/containerpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/containerpage.pt	(revision 10886)
@@ -0,0 +1,59 @@
+<form method="POST" i18n:domain="waeup.kofa">
+<br />
+  <input class="btn primary" type="submit" name="search"
+    tal:attributes="value view/search_button" />
+
+  <select name="searchtype">
+    <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">
+      who requested transcript (search term omitted)
+    </option>
+  </select>
+
+  <input type="text" name="searchterm" />
+
+  <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="display 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" class="gradeC">
+        <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>
+    <br />
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/courseticketmanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/courseticketmanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/courseticketmanagepage.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/browser_templates/courseticketpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/courseticketpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/courseticketpage.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/browser_templates/editscorespage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/editscorespage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/editscorespage.pt	(revision 10886)
@@ -0,0 +1,41 @@
+<form  i18n:domain="waeup.kofa"  method="POST">
+  <br />
+
+
+  <table class="display dataTable">
+    <thead>
+  	<tr>
+      <th i18n:translate="">Matric No.</th>
+      <th i18n:translate="">Score</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="">Course Level</th>
+  	</tr>
+    </thead>
+    <tbody>
+  	<tr tal:repeat="ticket view/tickets" class="gradeC">
+      <td tal:content="ticket/student/matric_number">MATRIC_NUMBER</td>
+  	  <td tal:condition="ticket/editable_by_lecturer">
+          <input type="text" name="scores" class="span1"
+  	             tal:attributes="value ticket/score" />
+      </td>
+      <td tal:condition="not: ticket/editable_by_lecturer"
+          tal:content="ticket/score">SCORE</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/student/current_session">SESSION</td>
+      <td tal:content="ticket/student/current_level">LEVEL</td>
+      <td tal:content="ticket/level">LEVEL</td>
+  	</tr>
+    </tbody>
+  </table>
+
+  <input type="submit" name="UPDATE"
+         value="Update scores" class="btn primary"
+         />
+
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/enterpin.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/enterpin.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/enterpin.pt	(revision 10886)
@@ -0,0 +1,41 @@
+<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" class="span1" maxlength="3"
+               value="" tal:attributes="value view/ac_series"/> -
+        <input name="ac_number" type="text" class="span3" maxlength="10"
+               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 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 primary"
+           tal:attributes="value view/buttonname" />
+  </span>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/exportconfig.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/exportconfig.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/exportconfig.pt	(revision 10886)
@@ -0,0 +1,66 @@
+<p class="alert-message 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>
+  <div class="input">
+    <select name="exporter">
+      <option tal:repeat="item view/exporters"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">Students</span>
+      </option>
+    </select>
+  </div>
+  <br />
+  <label for="session" i18n:translate="">
+    Current Session:
+  </label>
+  <div class="input">
+    <select name="session">
+      <option tal:repeat="item view/sessions"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">2012/2013</span>
+      </option>
+    </select>
+  </div>
+  <br />
+  <label for="level" i18n:translate="">
+    Current Level:
+  </label>
+  <div class="input">
+    <select name="level">
+      <option tal:repeat="item view/levels"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">200</span>
+      </option>
+    </select>
+  </div>
+  <br />
+  <label for="mode" i18n:translate="">
+    Current Study Mode:
+  </label>
+  <div class="input">
+    <select name="mode">
+      <option tal:repeat="item view/modes"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">ug_ft</span>
+      </option>
+    </select>
+  </div>
+  <br /><br />
+  <div class="input">
+  <input type="submit" name="START"  i18n:translate=""
+    value="Create CSV file"
+    class="btn primary" />
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/exportconfig_certificate.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/exportconfig_certificate.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/exportconfig_certificate.pt	(revision 10886)
@@ -0,0 +1,54 @@
+<p class="alert-message 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>
+  <div class="input">
+    <select name="exporter">
+      <option tal:repeat="item view/exporters"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">Students</span>
+      </option>
+    </select>
+  </div>
+  <br />
+  <label for="session" i18n:translate="">
+    Current Session:
+  </label>
+  <div class="input">
+    <select name="session">
+      <option tal:repeat="item view/sessions"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">2012/2013</span>
+      </option>
+    </select>
+  </div>
+  <br />
+  <label for="level" i18n:translate="">
+    Current Level:
+  </label>
+  <div class="input">
+    <select name="level">
+      <option tal:repeat="item view/levels"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">200</span>
+      </option>
+    </select>
+  </div>
+  <br /><br />
+  <div class="input">
+  <input type="submit" name="START"  i18n:translate=""
+    value="Create CSV file"
+    class="btn primary" />
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/exportconfig_course.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/exportconfig_course.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/exportconfig_course.pt	(revision 10886)
@@ -0,0 +1,43 @@
+<form i18n:domain="waeup.kofa" method="POST">
+  <label for="mode" i18n:translate="">
+    Exporter (Data Type):
+  </label>
+  <div class="input">
+    <select name="exporter">
+      <option tal:repeat="item view/exporters"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">Students</span>
+      </option>
+    </select>
+  </div>
+  <br />
+  <label for="session" i18n:translate="">
+    Level Session:
+  </label>
+  <div class="input">
+    <select name="session">
+      <option tal:repeat="item view/sessions"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">2012/2013</span>
+      </option>
+    </select>
+  </div>
+  <br />
+  <label for="level" i18n:translate="">
+    Level:
+  </label>
+  <div class="input">
+    <select name="level">
+      <option tal:repeat="item view/levels"
+              tal:attributes="value python:item[1]">
+        <span tal:replace="python: item[0]">200</span>
+      </option>
+    </select>
+  </div>
+  <br /><br />
+  <div class="input">
+  <input type="submit" name="START"  i18n:translate=""
+    value="Create CSV file"
+    class="btn primary" />
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/exportjobsindex.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/exportjobsindex.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/exportjobsindex.pt	(revision 10886)
@@ -0,0 +1,62 @@
+<table i18n:domain="waeup.kofa">
+  <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 nowrap>
+        <span tal:replace="job/start_time">DATETIME</span>
+      </td>
+      <td>
+        <span tal:replace="job/status">STATUS</span>
+      </td>
+      <td nowrap>
+        <a href="" class="btn" i18n:translate=""
+           tal:condition="job/show_refresh_button">
+          <img tal:attributes="src static/actionicon_reload.png" />
+          Reload
+        </a>
+        <form method="POST">
+          <a href="" class="btn primary small"
+                 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 small"
+                 name="DISCARD" value="Discard"
+                 tal:condition="job/show_discard_button" />
+        </form>
+      </td>
+    </tr>
+  </tbody>
+</table>
+<form method="POST" i18n:domain="waeup.kofa">
+  <div class="span12">
+
+  </div>
+  <input class="btn primary" type="submit" name="CREATE"
+         value="Configure new export"
+          />
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/filesuploadpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/filesuploadpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/filesuploadpage.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/browser_templates/filterstudentspage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/filterstudentspage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/filterstudentspage.pt	(revision 10886)
@@ -0,0 +1,88 @@
+<form  i18n:domain="waeup.kofa"  method="POST">
+  <br />
+  <label for="session">
+    <span tal:replace="view/session_label">SESSION</span>:
+  </label>
+  <div class="input">
+    <select name="session">
+      <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>
+  </div>
+  <br />
+  <label for="levels">
+    <span tal:replace="view/level_label">LEVEL</span>:
+  </label>
+  <div class="input">
+  <select name="level">
+    <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>
+  </div>
+  <br /><br />
+  <div class="input">
+    <input i18n:translate="" type="submit" class="btn primary"
+           name="SHOW" value="Show" />
+    <a class="btn secondary" href=""
+       tal:attributes="href view/parent_url">Cancel</a>
+  </div>
+
+  <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="display 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" class="gradeC">
+    	  <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/tags/0.2/src/waeup/kofa/students/browser_templates/imagedisplay.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/imagedisplay.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/imagedisplay.pt	(revision 10886)
@@ -0,0 +1,9 @@
+<tr>
+  <td class="fieldname">
+        <span tal:replace="viewlet/label">FILENAME</span>:
+  </td>
+  <td>
+      <img height="125px" tal:attributes="src viewlet/download_name" />
+      <br />
+  </td>
+</tr>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/imageupload.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/imageupload.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/imageupload.pt	(revision 10886)
@@ -0,0 +1,25 @@
+<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 viewlet/download_name" />
+    <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 error" />
+    <br /><br />
+    <input type="file" tal:attributes="name viewlet/input_name"/>
+    <br /><br />
+    <input type="submit"
+             tal:attributes="id python:('upload_%s' % viewlet.input_name);
+                name python:('upload_%s' % viewlet.input_name);
+                value viewlet/upload_button"
+             value="Upload" class="btn primary" />
+    <span i18n:translate="">
+      Max. file size:
+    </span>
+    <span tal:replace="viewlet/max_upload_size">10 KB</span>
+  </td>
+</tr>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/loginasstep1.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/loginasstep1.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/loginasstep1.pt	(revision 10886)
@@ -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/context/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/tags/0.2/src/waeup/kofa/students/browser_templates/loginasstep2.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/loginasstep2.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/loginasstep2.pt	(revision 10886)
@@ -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 primary" type="submit" name="SUBMIT"
+    tal:attributes="value view/login_button" />
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/mydatadropdowntabs.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/mydatadropdowntabs.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/mydatadropdowntabs.pt	(revision 10886)
@@ -0,0 +1,10 @@
+<li tal:attributes="class viewlet/active" data-dropdown="dropdown" >
+  <a href="#" class="dropdown-toggle"
+    tal:content="viewlet/tab_title">Tab Title</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/tags/0.2/src/waeup/kofa/students/browser_templates/onlinepaymentaddform.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/onlinepaymentaddform.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/onlinepaymentaddform.pt	(revision 10886)
@@ -0,0 +1,25 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      enctype="multipart/form-data">
+  <table class="form-table">
+    <tbody>
+      <tr>
+        <td class="fieldname">
+          <span>Payment Category</span>:
+        </td>
+        <td>
+          <select id="form.p_category" name="form.p_category" size="1" >
+            <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>
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/paymentsmanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/paymentsmanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/paymentsmanagepage.pt	(revision 10886)
@@ -0,0 +1,45 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <table class="display 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 cl/__name__">
+        <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 nowrap 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/tags/0.2/src/waeup/kofa/students/browser_templates/requestpw.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/requestpw.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/requestpw.pt	(revision 10886)
@@ -0,0 +1,28 @@
+<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" />
+      <tr tal:condition="view/captcha_code">
+        <td i18n:translate="">
+          Please solve the captcha<br />to prevent misuse of this service:
+        </td>
+        <td>
+          <div tal:content="structure view/captcha_code"></div>
+        </td>
+      </tr>
+
+    </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:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/requestpwmailsent.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/requestpwmailsent.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/requestpwmailsent.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/browser_templates/reverttransfer.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/reverttransfer.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/reverttransfer.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/browser_templates/setpassword.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/setpassword.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/setpassword.pt	(revision 10886)
@@ -0,0 +1,46 @@
+<form i18n:domain="waeup.kofa" method="POST">
+  <table class="form-table">
+    <tbody>
+      <tr>
+      <td class="fieldname" i18n:translate="">
+          Registration Number:
+      </td>
+      <td>
+        <input class="span5" 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 class="span1" name="ac_series" type="text"
+                 value="" tal:attributes="value view/ac_series"/> -
+          <input class="span3" name="ac_number" type="text"
+                 value="" tal:attributes="value view/ac_number" /> </td>
+      </tr>
+    </tbody>
+  </table>
+  <input type="submit" name="SUBMIT" class="btn primary"
+         tal:attributes="value view/set_button" />
+</form>
+<div i18n:domain="waeup.kofa" >
+  <h3 i18n:translate="">Instructions:</h3>
+  <ul class="list" 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/tags/0.2/src/waeup/kofa/students/browser_templates/studenthistory.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studenthistory.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studenthistory.pt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/browser_templates/studycoursemanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studycoursemanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studycoursemanagepage.pt	(revision 10886)
@@ -0,0 +1,91 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <ul class="tabs" data-tabs="tabs">
+    <li tal:attributes="class view/tab1"><a href="#tab-1">
+      <span i18n:translate="">Study Course Data</span></a>
+    </li>
+    <li tal:attributes="class view/tab2"><a href="#tab-2">
+      <span i18n:translate="">Study Levels</span></a>
+    </li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+      <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="tab-2" tal:attributes="class view/tab2">
+      <h3 i18n:translate="">Study Levels (Course Lists)</h3>
+      <table class="display 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 cl/__name__">
+              <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>
+        <span 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>
+        </span>
+        <select id="addlevel" name="addlevel" class='span5'>
+          <option tal:attributes="value level/code" tal:repeat="level view/level_dicts">
+            <span tal:replace="level/title">LEVELTITLE
+          </span>
+          </option>
+        </select>
+        &nbsp;&nbsp;<span i18n:translate="">in session</span>&nbsp;&nbsp;
+        <select id="level_session" name="level_session" class='span3'>
+          <option tal:attributes="value session/code" tal:repeat="session view/session_dicts">
+            <span tal:replace="session/title">LEVELSESSION
+          </span>
+          </option>
+        </select>
+      </div>
+    </div>
+  </div>
+</form>  
+
+
+
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studycoursepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studycoursepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studycoursepage.pt	(revision 10886)
@@ -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">
+  <thead>
+    <tr>
+      <th i18n:translate="">Level Code</th>
+      <th i18n:translate="">Level Title</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr class="gradeC" tal:repeat="value context/values">
+      <td> <a tal:attributes="href value/__name__">
+      <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/tags/0.2/src/waeup/kofa/students/browser_templates/studyleveladdpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studyleveladdpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studyleveladdpage.pt	(revision 10886)
@@ -0,0 +1,15 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <p i18n:translate="">Text to explain course registration procedure.</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/tags/0.2/src/waeup/kofa/students/browser_templates/studyleveleditpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studyleveleditpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studyleveleditpage.pt	(revision 10886)
@@ -0,0 +1,75 @@
+<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="display 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>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="value view/translated_values" class="gradeC">
+       <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/__name__">
+      <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>
+    </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 />
+  <table class="input-table">
+    <tr>
+      <td>
+        <input class="btn primary" type="submit" name="ADD"
+               i18n:translate=""
+               value="Add course ticket" />
+      </td>
+      <td>
+        <input type="text" name="course" />
+        <div class="hint" i18n:translate="">
+            Enter valid course code.
+        </div>
+      </td>
+    </tr>
+  </table>
+  <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/tags/0.2/src/waeup/kofa/students/browser_templates/studylevelmanagepage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studylevelmanagepage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studylevelmanagepage.pt	(revision 10886)
@@ -0,0 +1,103 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data">
+
+  <ul class="tabs" data-tabs="tabs">
+    <li tal:attributes="class view/tab1"><a href="#tab-1">
+      <span i18n:translate="">Study Level Data</span></a>
+    </li>
+    <li tal:attributes="class view/tab2"><a href="#tab-2">
+      <span i18n:translate="">Course Tickets</span>
+    </a></li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+      <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="tab-2" tal:attributes="class view/tab2">
+      <h3 i18n:translate="">Course Tickets</h3>
+      <table class="display 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="">Auto</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="value view/translated_values" class="gradeC">
+          <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/__name__">
+          <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/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 />
+      <table class="input-table">
+        <tr>
+          <td>
+            <input class="btn primary" type="submit" name="ADD"
+                   i18n:translate=""
+                   value="Add course ticket" />
+          </td>
+          <td>
+            <input type="text" name="course" />
+            <div class="hint" i18n:translate="">
+                Enter valid course code.
+            </div>
+          </td>
+        </tr>
+      </table>
+      <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/tags/0.2/src/waeup/kofa/students/browser_templates/studylevelpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studylevelpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/studylevelpage.pt	(revision 10886)
@@ -0,0 +1,61 @@
+<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>
+  </tbody>
+</table>
+
+<h3 i18n:domain="waeup.kofa" i18n:translate="">Course Tickets</h3>
+
+<table class="display 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="">Auto</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="value view/translated_values" class="gradeC">
+      <td tal:content="value/semester" nowrap>TERM</td>
+      <td> <a tal:attributes="href value/__name__">
+      <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/automatic">AUTO</td>
+    </tr>
+  </tbody>
+</table>
+
+
+<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/tags/0.2/src/waeup/kofa/students/browser_templates/transcript.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/transcript.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/transcript.pt	(revision 10886)
@@ -0,0 +1,60 @@
+<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 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 class="filedname">
+        <span i18n:translate="">Cumulative GPA</span>:
+      </td>
+      <td>
+        <div tal:content="python:tdata[1]">GPA</div>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+<h3 i18n:domain="waeup.kofa" i18n:translate="">Subjects/Courses</h3>
+
+<table i18n:domain="waeup.kofa">
+  <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" class="gradeC">
+        <td tal:content="python: view.level_dict[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>
+</tal:def>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/transcriptprocess.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/transcriptprocess.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/transcriptprocess.pt	(revision 10886)
@@ -0,0 +1,39 @@
+<form method="POST" i18n:domain="waeup.kofa">
+
+  <ul class="tabs" data-tabs="tabs">
+    <li tal:attributes="class view/tab1"><a href="#tab-1">
+    <span i18n:translate="">Process request</span></a></li>
+    <li tal:attributes="class view/tab2"><a href="#tab-2">
+    <span i18n:translate="">View correspondence</span></a></li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+
+      <table class="form-table">
+        <tbody>
+          <tr>
+            <td 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 primary"
+                     tal:attributes="value view/buttonname" />
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <div id="tab-2" tal:attributes="class view/tab2">
+      <p tal:content="structure view/correspondence"></p>
+    </div>
+  </div>
+
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/transcriptrequest.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/transcriptrequest.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/transcriptrequest.pt	(revision 10886)
@@ -0,0 +1,72 @@
+<form method="POST" i18n:domain="waeup.kofa">
+
+  <ul class="tabs" data-tabs="tabs">
+    <li tal:attributes="class view/tab1"><a href="#tab-1">
+    <span i18n:translate="">Send new request</span></a></li>
+    <li tal:attributes="class view/tab2"><a href="#tab-2">
+    <span i18n:translate="">View correspondence</span></a></li>
+  </ul>
+
+  <div class="tab-content">
+    <div id="tab-1" tal:attributes="class view/tab1">
+
+      <p i18n:translate="transcript_request"></p>
+
+      <table class="form-table" tal:condition="view/with_ac">
+        <tbody>
+          <tr>
+            <td i18n:translate="">
+            Cover Letter:
+            </td>
+            <td>
+            <textarea cols="60" name="comment" rows="6"></textarea>
+            </td>
+          </tr>
+          <tr>
+            <td i18n:translate="">
+            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" class="span1" maxlength="3"
+                   value="" tal:attributes="value view/ac_series"/> -
+            <input name="ac_number" type="text" class="span3" maxlength="10"
+                   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 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 primary"
+               tal:attributes="value view/buttonname" />
+      </span>
+    </div>
+
+    <div id="tab-2"  tal:attributes="class view/tab2">
+      <p tal:content="structure view/correspondence"></p>
+    </div>
+  </div>
+
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/trigtrans.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/trigtrans.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/browser_templates/trigtrans.pt	(revision 10886)
@@ -0,0 +1,18 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.kofa" enctype="multipart/form-data"
+      autocomplete="off">
+
+
+    <select id="transition" name="transition">
+      <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/tags/0.2/src/waeup/kofa/students/catalog.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/catalog.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/catalog.py	(revision 10886)
@@ -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
+
+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 == 'transcript':
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(
+            state=('transcript requested', 'transcript requested'))
+    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/tags/0.2/src/waeup/kofa/students/container.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/container.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/container.py	(revision 10886)
@@ -0,0 +1,82 @@
+## $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 archive(self, id=None):
+        raise NotImplementedError()
+
+    def clear(self, id=None, archive=True):
+        raise NotImplementedError()
+
+    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/tags/0.2/src/waeup/kofa/students/dynamicroles.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/dynamicroles.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/dynamicroles.py	(revision 10886)
@@ -0,0 +1,116 @@
+## $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',
+        }
+
+    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 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/tags/0.2/src/waeup/kofa/students/export.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/export.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/export.py	(revision 10886)
@@ -0,0 +1,490 @@
+## $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
+from zope.component import getUtility
+from waeup.kofa.interfaces import IExtFileStore, IFileStoreNameChooser
+from waeup.kofa.interfaces import MessageFactory as _
+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
+
+#: A tuple containing all exporter names referring to students or
+#: subobjects thereof.
+EXPORTER_NAMES = ('students', 'studentstudycourses',
+        'studentstudylevels', 'coursetickets',
+        'studentpayments', 'bedtickets', 'paymentsoverview',
+        'studylevelsoverview', 'combocard', 'bursary')
+
+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):
+    """Get all studylevels of `students`.
+    """
+    levels = []
+    for course in get_studycourses(students):
+        for level in course.values():
+            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 meets level
+    and level_session.
+    """
+    tickets = []
+    code = kw.get('code', None)
+    level = kw.get('level', None)
+    level_session = kw.get('level_session', None)
+    if code is None:
+        for level_obj in get_levels(students):
+            for ticket in level_obj.values():
+                tickets.append(ticket)
+    else:
+        for level_obj in get_levels(students):
+            for ticket in level_obj.values():
+                if ticket.code != code:
+                    continue
+                if level is not None and int(level) != level_obj.level:
+                    continue
+                if level_session is not None and \
+                    int(level_session) != level_obj.level_session:
+                    continue
+                tickets.append(ticket)
+    return tickets
+
+def get_payments(students):
+    """Get all payments of `students`.
+    """
+    payments = []
+    for student in students:
+        for payment in student.get('payments', {}).values():
+            payments.append(payment)
+    return payments
+
+def get_paid_payments(students):
+    """Get all paid payments of `students`.
+    """
+    payments = []
+    for student in students:
+        for payment in student.get('payments', {}).values():
+            if payment.p_state == 'paid':
+                payments.append(payment)
+    return payments
+
+def get_bedtickets(students):
+    """Get all bedtickets 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))
+        query = StudentsQuery(**kw)
+        return query.query()
+
+    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)
+
+
+class StudentsExporter(grok.GlobalUtility, StudentExporterBase):
+    """Exporter for Students.
+    """
+    grok.name('students')
+
+    #: Fieldnames considered by this exporter
+    fields = tuple(sorted(iface_names(IStudent))) + (
+        'password', 'state', 'history', 'certcode', 'is_postgrad',
+        'current_level', 'current_session')
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Students')
+
+    def mangle_value(self, value, name, context=None):
+        if name == 'history':
+            value = value.messages
+        if name == 'phone' and value is not None:
+            # Append hash '#' to phone numbers to circumvent
+            # unwanted excel automatic
+            value = str('%s#' % value)
+        return super(
+            StudentsExporter, self).mangle_value(
+            value, name, context=context)
+
+
+class StudentStudyCourseExporter(grok.GlobalUtility, StudentExporterBase):
+    """Exporter for StudentStudyCourses.
+    """
+    grok.name('studentstudycourses')
+
+    #: Fieldnames considered by this exporter
+    fields = tuple(sorted(iface_names(IStudentStudyCourse))) + ('student_id',)
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Student Study Courses')
+
+    def filter_func(self, x, **kw):
+        return get_studycourses(x)
+
+    def mangle_value(self, value, name, context=None):
+        """Treat location values special.
+        """
+        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):
+    """Exporter for StudentStudyLevels.
+    """
+    grok.name('studentstudylevels')
+
+    #: Fieldnames considered by this exporter
+    fields = tuple(sorted(iface_names(
+        IStudentStudyLevel) + ['level'])) + (
+        'student_id', 'number_of_tickets','certcode')
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Student Study Levels')
+
+    def filter_func(self, x, **kw):
+        return get_levels(x)
+
+    def mangle_value(self, value, name, context=None):
+        """Treat location values special.
+        """
+        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):
+    """Exporter for CourseTickets.
+    """
+    grok.name('coursetickets')
+
+    #: Fieldnames considered by this exporter
+    fields = tuple(sorted(iface_names(ICourseTicket) +
+        ['level', 'code', 'level_session'])) + ('student_id', 'certcode')
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Course Tickets')
+
+    def filter_func(self, x, **kw):
+        return get_tickets(x, **kw)
+
+    def mangle_value(self, value, name, context=None):
+        """Treat location values special.
+        """
+        if context is not None:
+            student = context.student
+            if name == 'student_id' and student is not None:
+                value = getattr(student, name, None)
+            if name == 'level':
+                value = getattr(context, 'level', lambda: None)
+            if name == 'level_session':
+                value = getattr(context, 'level_session', lambda: None)
+        return super(
+            CourseTicketExporter, self).mangle_value(
+            value, name, context=context)
+
+
+class StudentPaymentsExporter(grok.GlobalUtility, StudentExporterBase):
+    """Exporter for OnlinePayment instances.
+    """
+    grok.name('studentpayments')
+
+    #: Fieldnames considered by this exporter
+    fields = tuple(
+        sorted(iface_names(
+            IStudentOnlinePayment, exclude_attribs=False,
+            omit=['display_item']))) + (
+            'student_id','state','current_session')
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Student Payments')
+
+    def filter_func(self, x, **kw):
+        return get_payments(x)
+
+    def mangle_value(self, value, name, context=None):
+        """Treat location values special.
+        """
+        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(
+            StudentPaymentsExporter, self).mangle_value(
+            value, name, context=context)
+
+class DataForBursaryExporter(StudentPaymentsExporter):
+    """Exporter for OnlinePayment instances.
+    """
+    grok.name('bursary')
+
+    def filter_func(self, x, **kw):
+        return get_paid_payments(x)
+
+    #: Fieldnames considered by this exporter
+    fields = tuple(
+        sorted(iface_names(
+            IStudentOnlinePayment, exclude_attribs=False,
+            omit=['display_item']))) + (
+            'student_id','matric_number',
+            'firstname', 'middlename', 'lastname',
+            'state','current_session',
+            'entry_session', 'entry_mode',
+            'faccode', 'depcode')
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Payment Data for Bursary')
+
+    def mangle_value(self, value, name, context=None):
+        """Treat location values special.
+        """
+        if context is not None:
+            student = context.student
+            if name in [
+                'student_id','matric_number',
+                'firstname', 'middlename', 'lastname',
+                'state','current_session',
+                'entry_session', 'entry_mode',
+                'faccode', 'depcode'] and student is not None:
+                value = getattr(student, name, None)
+        return super(
+            StudentPaymentsExporter, self).mangle_value(
+            value, name, context=context)
+
+class BedTicketsExporter(grok.GlobalUtility, StudentExporterBase):
+    """Exporter for BedTicket instances.
+    """
+    grok.name('bedtickets')
+
+    #: Fieldnames considered by this exporter
+    fields = tuple(
+        sorted(iface_names(
+            IBedTicket, exclude_attribs=False,
+            omit=['display_coordinates']))) + (
+            'student_id', 'actual_bed_type')
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Bed Tickets')
+
+    def filter_func(self, x, **kw):
+        return get_bedtickets(x)
+
+    def mangle_value(self, value, name, context=None):
+        """Treat location values and others special.
+        """
+        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')
+        return super(
+            BedTicketsExporter, self).mangle_value(
+            value, name, context=context)
+
+class StudentPaymentsOverviewExporter(StudentsExporter):
+    """Exporter for students with payment overview.
+    """
+    grok.name('paymentsoverview')
+
+    curr_year = datetime.now().year
+    year_range = range(curr_year - 9, curr_year + 1)
+    year_range_tuple = tuple([str(year) for year in year_range])
+
+    #: Fieldnames considered by this exporter
+    fields = ('student_id', 'matric_number', 'display_fullname',
+        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
+        'current_level', 'current_session', 'current_mode',
+        ) + year_range_tuple
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Student Payments Overview')
+
+    def mangle_value(self, value, name, context=None):
+        if name in self.year_range_tuple and context is not None:
+            value = ''
+            for ticket in context['payments'].values():
+                if ticket.p_state == 'paid' and \
+                    ticket.p_category == 'schoolfee' and \
+                    ticket.p_session == int(name):
+                    value = ticket.amount_auth
+                    break
+        return super(
+            StudentsExporter, self).mangle_value(
+            value, name, context=context)
+
+class StudentStudyLevelsOverviewExporter(StudentsExporter):
+    """Exporter for students with study level overview.
+    """
+    grok.name('studylevelsoverview')
+
+    avail_levels = tuple([str(x) for x in study_levels(None)])
+
+    #: Fieldnames considered by this exporter
+    fields = ('student_id', ) + (
+        'state', 'certcode', 'faccode', 'depcode', 'is_postgrad',
+        'entry_session', 'current_level', 'current_session',
+        ) + avail_levels
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Student Study Levels Overview')
+
+    def mangle_value(self, value, name, context=None):
+        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|%s|%s|%s' % (
+                    #    level.level_session,
+                    #    len(level),
+                    #    level.validated_by,
+                    #    level.level_verdict)
+                    value = '%s' % level.level_session
+                    break
+        return super(
+            StudentsExporter, self).mangle_value(
+            value, name, context=context)
+
+class ComboCardDataExporter(grok.GlobalUtility, StudentExporterBase):
+    """Exporter for Interswitch Combo Card Data.
+    """
+    grok.name('combocard')
+
+    #: Fieldnames considered by this exporter
+    fields = ('display_fullname',
+              'student_id','matric_number',
+              'certificate', 'faculty', 'department', 'passport_path')
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Combo Card Data')
+
+    def mangle_value(self, value, name, context=None):
+        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)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/interfaces.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/interfaces.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/interfaces.py	(revision 10886)
@@ -0,0 +1,860 @@
+## $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
+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)
+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,
+    ContextualDictSourceFactoryBase)
+
+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(IStudentsUtils).VERDICTS_DICT
+        return sorted(verdicts_dict.keys())
+
+    def getToken(self, context, value):
+        return value
+
+    def getTitle(self, context, value):
+        verdicts_dict = getUtility(IStudentsUtils).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,):
+        """Create Payment object and set the payment data of a student for
+        the payment category specified.
+
+        """
+
+    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 getPDFCreator(context):
+        """Get some IPDFCreator instance suitable for use with `context`.
+        """
+
+    def renderPDF(view, subject='', filename='slip.pdf',):
+        """Render pdf slips for various pages.
+
+        """
+
+class IStudentsContainer(IKofaObject):
+    """A students container contains university students.
+
+    """
+    def addStudent(student):
+        """Add an IStudent object and subcontainers.
+
+        """
+
+    def archive(id=None):
+        """Create on-dist archive of students.
+
+        If id is `None`, all students are archived.
+
+        If id contains a single id string, only the respective
+        students are archived.
+
+        If id contains a list of id strings all of the respective
+        students types are saved to disk.
+        """
+
+    def clear(id=None, archive=True):
+        """Remove students of type given by 'id'.
+
+        Optionally archive the students.
+
+        If id is `None`, all students are archived.
+
+        If id contains a single id string, only the respective
+        students are archived.
+
+        If id contains a list of id strings all of the respective
+        student types are saved to disk.
+
+        If `archive` is ``False`` none of the archive-handling is done
+        and respective students are simply removed from the
+        database.
+        """
+
+    unique_student_id = Attribute("""A unique student id.""")
+
+class IStudentNavigation(IStudentNavigationBase):
+    """Interface needed for student navigation, logging, etc.
+
+    """
+    student = Attribute('Student object of context.')
+
+    def writeLogMessage(view, message):
+        """Write a view specific log message into students.log.
+
+        """
+
+class IStudentBase(IKofaObject):
+    """Representation of student base data.
+
+    """
+    history = Attribute('Object history, a list of messages')
+    state = Attribute('Returns the registration state of a student')
+    password = Attribute('Encrypted password of a student')
+    temp_password = Attribute(
+        'Dictionary with user name, timestamp and encrypted password')
+    certcode = Attribute('The certificate code of any chosen study course')
+    depcode = Attribute('The department code of any chosen study course')
+    faccode = Attribute('The faculty code of any chosen study course')
+    entry_session = Attribute('The entry session of the student')
+    current_session = Attribute('The current session of the student')
+    current_level = Attribute('The current level of the student')
+    current_mode = Attribute('The current mode of the student')
+    current_verdict = Attribute('The current verdict of the student')
+    fullname = Attribute('All name parts separated by hyphens')
+    display_fullname = Attribute('The fullname of an applicant')
+    is_postgrad = Attribute('True if postgraduate student')
+
+    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.'),
+        )
+
+    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'Sex'),
+        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'),
+        description = u'',
+        required = False,
+        )
+
+    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 transfer(certificate, current_session,
+        current_level, current_verdict):
+        """ Creates a new studycourse and backups the old one.
+
+        """
+
+class IUGStudentClearance(IKofaObject):
+    """Representation of undergraduate student clearance data.
+
+    """
+    officer_comment = schema.Text(
+        title = _(u"Officer's Comment"),
+        required = False,
+        )
+
+    clearance_locked = schema.Bool(
+        title = _(u'Clearance form locked'),
+        default = False,
+        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 IStudentTranscript(IKofaObject):
+    """Representation of student transcript data.
+
+    """
+
+    transcript_comment = schema.Text(
+        title = _(u'Comment'),
+        required = False,
+        )
+
+
+class IStudent(IStudentBase,IUGStudentClearance,IPGStudentClearance,
+    IStudentPersonal, IStudentTranscript):
+    """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 an 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,
+        )
+
+    firstname = schema.TextLine(
+        title = _(u'First Name'),
+        required = True,
+        )
+
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        )
+
+class IStudentStudyCourse(IKofaObject):
+    """A container for student study levels.
+
+    """
+    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,
+        )
+
+class IStudentStudyCourseTransfer(IStudentStudyCourse):
+    """An 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 IStudentStudyCourseTranscript(IKofaObject):
+    """An interface for student transcripts.
+
+    """
+
+    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,
+        )
+
+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 container for course tickets.
+
+    """
+    level = Attribute('The level code')
+    number_of_tickets = Attribute('Number of tickets contained in this level')
+    certcode = Attribute('The certificate code of the study course')
+    is_current_level = Attribute('Is this level the current level of the student?')
+    passed_params = Attribute('Information about passed and failed courses')
+
+    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.Int(
+        title = _(u'Unrectified GPA'),
+        required = False,
+        readonly = True,
+        )
+
+    def addCourseTicket(ticket, course):
+        """Add a course ticket object.
+        """
+
+    def addCertCourseTickets(cert):
+        """Collect all certificate courses and create course
+        tickets automatically.
+        """
+
+class ICourseTicket(IKofaObject):
+    """An interface for course tickets.
+
+    """
+    code = Attribute('code of the original course')
+    certcode = Attribute('certificate code of the study course')
+    grade = Attribute('grade calculated from score')
+    weight = Attribute('weight calculated from score')
+    removable_by_student = Attribute('Is student allowed to remove the ticket?')
+    level_session = Attribute('session of the level the ticket has been added to')
+    level = Attribute('id of the level the ticket has been added to')
+
+    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,
+        )
+
+    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,
+        )
+
+    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.
+
+    """
+
+    def addBedTicket(bedticket):
+        """Add a bed ticket object.
+        """
+
+
+class IBedTicket(IKofaObject):
+    """A ticket for accommodation booking.
+
+    """
+    bed = Attribute('The bed object.')
+
+    bed_coordinates = schema.TextLine(
+        title = u'',
+        required = True,
+        readonly = False,
+        )
+
+    display_coordinates = schema.TextLine(
+        title = _(u'Allocated Bed'),
+        required = False,
+        readonly = True,
+        )
+
+    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 = True,
+        )
+
+    booking_date = schema.Datetime(
+        title = _(u'Booking Date'),
+        required = False,
+        readonly = True,
+        )
+
+    booking_code = schema.TextLine(
+        title = _(u'Booking Activation Code'),
+        required = False,
+        readonly = True,
+        )
+
+    def getSessionString():
+        """Returns the title of academic_sessions_vocab term.
+
+        """
+
+class IStudentPaymentsContainer(IPaymentsContainer):
+    """A container for student payment objects.
+
+    """
+
+class IStudentOnlinePayment(IOnlinePayment):
+    """A student payment via payment gateways.
+
+    """
+
+    p_current = schema.Bool(
+        title = _(u'Current Session Payment'),
+        default = True,
+        required = False,
+        )
+
+    p_level = schema.Int(
+        title = _(u'Payment Level'),
+        required = False,
+        )
+
+    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 export_student(student, filepath=None):
+        """Export data for a given student.
+        """
+
+    def export_filtered(site, filepath=None, **kw):
+        """Export filtered set of students.
+        """
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/payments.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/payments.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/payments.py	(revision 10886)
@@ -0,0 +1,186 @@
+## $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 waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.students.interfaces import (
+    IStudentPaymentsContainer, IStudentNavigation, IStudentOnlinePayment)
+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__
+
+    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
+
+    def writeLogMessage(self, view, message):
+        return self.__parent__.__parent__.writeLogMessage(view, message)
+
+    def _createActivationCodes(self):
+        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 in ('schoolfee', 'schoolfee_1'):
+            # 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._createActivationCodes()
+            if error is not None:
+                return False, error, error
+        log = 'successful %s payment: %s' % (self.p_category, self.p_id)
+        msg = _('Successful payment')
+        return True, msg, log
+
+    def doAfterStudentPaymentApproval(self):
+        """Process student after payment was approved.
+        """
+        if self.p_current:
+            error = self._createActivationCodes()
+            if error is not None:
+                return False, error, error
+        log = '%s payment approved: %s' % (self.p_category, self.p_id)
+        msg = _('Payment approved')
+        return True, msg, log
+
+    def approveStudentPayment(self):
+        """Approve payment and process student.
+        """
+        if self.p_state == 'paid':
+            return False, _('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
+
+# 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/tags/0.2/src/waeup/kofa/students/permissions.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/permissions.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/permissions.py	(revision 10886)
@@ -0,0 +1,121 @@
+## $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 student section.
+"""
+import grok
+
+# Student section permissions
+
+class HandleStudent(grok.Permission):
+    grok.name('waeup.handleStudent')
+
+class ViewStudent(grok.Permission):
+    grok.name('waeup.viewStudent')
+
+class ViewStudentsTab(grok.Permission):
+    grok.name('waeup.viewStudentsTab')
+
+class ViewMyStudentDataTab(grok.Permission):
+    grok.name('waeup.viewMyStudentDataTab')
+
+class ViewStudentsContainer(grok.Permission):
+    grok.name('waeup.viewStudentsContainer')
+
+class PayStudent(grok.Permission):
+    grok.name('waeup.payStudent')
+
+class HandleAccommodation(grok.Permission):
+    grok.name('waeup.handleAccommodation')
+
+class UploadStudentFile(grok.Permission):
+    grok.name('waeup.uploadStudentFile')
+
+class ManageStudent(grok.Permission):
+    grok.name('waeup.manageStudent')
+
+class ClearStudent(grok.Permission):
+    grok.name('waeup.clearStudent')
+
+class ValidateStudent(grok.Permission):
+    grok.name('waeup.validateStudent')
+
+class EditStudyLevel(grok.Permission):
+    grok.name('waeup.editStudyLevel')
+
+class TriggerTransition(grok.Permission):
+    grok.name('waeup.triggerTransition')
+
+class LoginAsStudent(grok.Permission):
+    grok.name('waeup.loginAsStudent')
+
+# Local role
+class StudentRecordOwner(grok.Role):
+    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')
+
+# Site Roles
+class StudentRole(grok.Role):
+    grok.name('waeup.Student')
+    grok.title(u'Student (do not assign)')
+    grok.permissions('waeup.viewAcademics', 'waeup.viewMyStudentDataTab',
+                     'waeup.Authenticated')
+
+class StudentsOfficer(grok.Role):
+    grok.name('waeup.StudentsOfficer')
+    grok.title(u'Students Officer (view only)')
+    grok.permissions('waeup.viewStudent','waeup.viewStudents',
+          'waeup.viewStudentsTab', 'waeup.viewStudentsContainer')
+
+class StudentsManager(grok.Role):
+    grok.name('waeup.StudentsManager')
+    grok.title(u'Students Manager')
+    grok.permissions('waeup.viewStudent', 'waeup.viewStudents',
+                     'waeup.manageStudent', 'waeup.viewStudentsContainer',
+                     'waeup.payStudent', 'waeup.uploadStudentFile',
+                     'waeup.viewStudentsTab', 'waeup.handleAccommodation')
+
+class TranscriptOfficer(grok.Role):
+    grok.name('waeup.TranscriptOfficer')
+    grok.title(u'Transcript Officer')
+    grok.permissions('waeup.viewAcademics',
+                     'waeup.viewTranscript',
+                     'waeup.viewStudent',
+                     'waeup.viewStudents',
+                     'waeup.viewStudentsTab',
+                     'waeup.viewStudentsContainer',
+                     )
+
+class StudentsClearanceOfficer(grok.Role):
+    grok.name('waeup.StudentsClearanceOfficer')
+    grok.title(u'Clearance Officer (all students)')
+    grok.permissions('waeup.clearStudent','waeup.viewStudent')
+
+class StudentsCourseAdviser(grok.Role):
+    grok.name('waeup.StudentsCourseAdviser')
+    grok.title(u'Course Adviser (all students)')
+    grok.permissions('waeup.validateStudent','waeup.viewStudent',
+                     'waeup.editStudyLevel')
+
+class StudentImpersonator(grok.Role):
+    grok.name('waeup.StudentImpersonator')
+    grok.title(u'Student Impersonator')
+    grok.permissions('waeup.loginAsStudent')
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/__init__.py	(revision 10886)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/browser_templates/levelreportgeneratorpage.pt	(revision 10886)
@@ -0,0 +1,65 @@
+<form  i18n:domain="waeup.kofa"  method="POST">
+  <p i18n:translate="">
+    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.
+  </p>
+  <br />
+  <div class="clearfix">
+    <label for="session" i18n:translate="">Department:</label>
+    <div class="input">
+      <select name="faccode_depcode">
+        <span tal:repeat="items view/faccode_depcodes" 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 class="help-inline" i18n:translate="">
+        Set department filter.
+      </div>
+    </div>
+    <br />
+    <label for="session">Academic Session:</label>
+    <div class="input">
+      <select name="session" i18n:translate="">
+        <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>
+      <div class="help-inline" i18n:translate="">
+        Select academic session to report.
+      </div>
+    </div>
+    <br />
+    <label for="session">Level:</label>
+    <div class="input">
+      <select name="level" i18n:translate="">
+        <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>
+      <div class="help-inline" i18n:translate="">
+        Select level to report.
+      </div>
+    </div>
+
+  </div>
+  <div class="input">
+    <input i18n:translate="" type="submit" class="btn primary"
+           name="CREATE" value="Create" />
+    <a class="btn secondary" href=""
+       tal:attributes="href view/parent_url">Cancel</a>
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/browser_templates/studentstatisticsreportgeneratorpage.pt	(revision 10886)
@@ -0,0 +1,49 @@
+<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 />
+  <div class="clearfix">
+    <label for="session" i18n:translate="">Study Modes Group:</label>
+    <div class="input">
+      <select name="mode">
+        <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>
+      <div class="help-inline" i18n:translate="">
+        Set study modes filter.
+      </div>
+    </div>
+    <br />
+    <label for="session">Academic Session:</label>
+    <div class="input">
+      <select name="session" i18n:translate="">
+        <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>
+      <div class="help-inline" i18n:translate="">
+        Select academic session to report.
+      </div>
+    </div>
+
+  </div>
+  <div class="input">
+    <input i18n:translate="" type="submit" class="btn primary"
+           name="CREATE" value="Create" />
+    <a class="btn secondary" href=""
+       tal:attributes="href view/parent_url">Cancel</a>
+  </div>
+</form>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/level_report.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/level_report.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/level_report.py	(revision 10886)
@@ -0,0 +1,292 @@
+## $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.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.interfaces import MessageFactory as _
+from waeup.kofa.reports import IReport
+from waeup.kofa.browser.pdf import get_signature_tables
+from waeup.kofa.students.reports.student_statistics import (
+    StudentStatisticsReportPDFView)
+
+class ILevelReport(IReport):
+
+    session = Attribute('Session to report')
+    level = Attribute('Level to report')
+    faccode = Attribute('Faculty to report')
+    depcode = Attribute('Department to report')
+    creation_dt_string = Attribute('Human readable report creation datetime')
+
+def get_students(faccode, depcode, session, level):
+    """Get students in a certain department 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")
+    result = cat.searchResults(
+        depcode = (depcode, depcode), faccode = (faccode, faccode)
+        )
+    table = []
+    for stud in result:
+        if not stud['studycourse'].has_key(str(level)):
+            continue
+        level_obj = stud['studycourse'][str(level)]
+        if level_obj.level_session != session:
+            continue
+        passed_params = level_obj.passed_params
+        line = (
+                #stud.student_id,
+                stud.matric_number,
+                stud.display_fullname,
+                level_obj.total_credits,
+                passed_params[2],
+                level_obj.gpa,
+                passed_params[4],
+                level_obj.cumulative_params[3],
+                level_obj.cumulative_params[4],
+                level_obj.cumulative_params[0],
+                )
+        table.append(line)
+    return sorted(table, key=lambda value:value[0])
+
+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.university.vocabularies import course_levels
+
+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 = (
+            #'Student Id',
+            'S/N',
+            'Matric No.',
+            'Name',
+            TTR('Total Credits'),
+            TTR('Total Credits Passed'),
+            TTR('GPA'),
+            'Courses Failed',
+            TTR('Cum. Credits Taken'),
+            TTR('Cum. Credits Passed'),
+            TTR('CGPA'))
+    table = [col_names]
+    sn = 1
+    for line in data:
+        line = (sn,) + line
+        table.append(line)
+        sn += 1
+    return table
+
+TABLE_STYLE = [
+    ('FONT', (0,0), (-1,-1), 'Helvetica', 8),
+    #('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'),
+    ('INNERGRID', (0,1), (-1,-1), 0.25, colors.black),
+    ('BOX', (0,1), (-1,-1), 1, colors.black),
+    ]
+
+@implementer(ILevelReport)
+class LevelReport(Report):
+    data = None
+    session = None
+    level = None
+    faccode = None
+    depcode = None
+
+    def __init__(self, faccode, depcode, session, level, author='System'):
+        super(LevelReport, self).__init__(
+            args=[faccode, depcode, session, level], kwargs={'author':author})
+        site = grok.getSite()
+        self.session = academic_sessions_vocab.getTerm(session).title
+        self.level = course_levels.getTerm(level).title
+        self.levelcode = level
+        self.sessioncode = session
+        self.faccode = faccode
+        self.depcode = depcode
+        self.factitle = site['faculties'][faccode].longtitle
+        self.deptitle = site['faculties'][faccode][depcode].longtitle
+        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_students(faccode, depcode, session, level)
+
+    def create_pdf(self):
+        creator = getUtility(IPDFCreator, name='landscape')
+        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, 0.8*cm,
+                      6*cm, 0.8*cm, 0.8*cm, 0.8*cm]
+        pdf_data = [Paragraph('<b>%s</b>' % self.creation_dt_string,
+                              STYLE["Normal"]),
+                    Spacer(1, 12),]
+        pdf_data += [Paragraph('%s<br />%s<br />Level: %s<br />Session: %s' % (
+                    self.factitle, self.deptitle, self.level, self.session),
+                    STYLE["Normal"]),
+                    Spacer(1, 12),]
+        pdf_data += [
+            Table(table_data, style=TABLE_STYLE, colWidths=col_widths)]
+        doc_title = 'Level Report'
+
+        pdf_data.append(Spacer(1, 40))
+        signatures = ['Ag. Head of Department', 'Dean of Faculty']
+        signaturetables = get_signature_tables(signatures, landscape=True)
+        pdf_data.append(signaturetables[0])
+
+        pdf = creator.create_pdf(
+            pdf_data, None, doc_title, self.author,
+            'Level Report %s/%s/%s/%s' % (
+            self.faccode, self.depcode, self.levelcode, self.sessioncode)
+            )
+        return pdf
+
+@implementer(IReportGenerator)
+class LevelReportGenerator(grok.GlobalUtility):
+
+    title = _('Level Report')
+    grok.name('level_report')
+
+    def generate(self, site, faccode=None, depcode=None,
+                 session=None, level=None, author=None):
+        result = LevelReport(faccode=faccode, depcode=depcode,
+                             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
+grok.templatedir('browser_templates')
+class LevelReportGeneratorPage(KofaPage):
+
+    grok.context(LevelReportGenerator)
+    grok.name('index.html')
+    grok.require('waeup.manageReports')
+
+    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=None,
+               session=None, level=None):
+        self.parent_url = self.url(self.context.__parent__)
+        self._set_session_values()
+        self._set_level_values()
+        self._set_faccode_depcode_values()
+        if CREATE and session:
+            # create a new report job for students by session
+            faccode = faccode_depcode.split('_')[0]
+            depcode = faccode_depcode.split('_')[1]
+            container = self.context.__parent__
+            user_id = self.request.principal.id
+            if level is not None:
+                level = int(level)
+            if session is not None:
+                session = int(session)
+            kw = dict(
+                session=session,
+                level=level,
+                faccode=faccode,
+                depcode=depcode)
+            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, session=%s, level=%s)' % (
+                ob_class, job_id, self.context.title, faccode, depcode, 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_faccode_depcode_values(self):
+        faccode_depcodes = []
+        faculties = grok.getSite()['faculties']
+        for fac in faculties.values():
+            for dep in fac.values():
+                faccode_depcodes.append(
+                    ('%s (%s)' %(dep.longtitle, fac.longtitle),
+                     '%s_%s' %(fac.code, dep.code)))
+        self.faccode_depcodes = sorted(
+            faccode_depcodes, key=lambda value: value[0])
+        return
+
+class LevelReportPDFView(StudentStatisticsReportPDFView):
+
+    grok.context(ILevelReport)
+    grok.name('pdf')
+    grok.require('waeup.Public')
+    prefix = 'LevelReport'
+
+    def _filename(self):
+        return 'LevelReport_%s_%s_%s_%s_%s.pdf' % (
+            self.context.faccode, self.context.depcode,
+            self.context.sessioncode, self.context.levelcode,
+            self.context.creation_dt_string)
+
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/session_results_presentation.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/session_results_presentation.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/session_results_presentation.py	(revision 10886)
@@ -0,0 +1,231 @@
+## $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.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.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)
+
+
+class ISessionResultsPresentation(ILevelReport):
+
+    """ Same interface as for level reports.
+    """
+
+def get_students(faccode, depcode, session, level=None):
+    """Get students in a certain department 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")
+    result = cat.searchResults(
+        depcode = (depcode, depcode), faccode = (faccode, faccode)
+        )
+    students_utils = getUtility(IStudentsUtils)
+    table = []
+    for i in range(len(students_utils.gpa_boundaries)+1):
+        # The last list is reserved for students with more than one
+        # level in the same session.
+        table.append([])
+    for stud in result:
+        line = (stud.student_id,
+                stud.matric_number,
+                stud.display_fullname,
+                )
+        if level is not None:
+            if not stud['studycourse'].has_key(str(level)):
+                continue
+            level_obj = stud['studycourse'][str(level)]
+            if level_obj.level_session != session:
+                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 session registered in this session
+                table[len(students_utils.gpa_boundaries)].append(line)
+                continue
+        if level_obj.gpa_params[1] == 0:
+            # Error: no credits weighted
+            table[len(students_utils.gpa_boundaries)].append(line)
+            continue
+        # Session GPA can be determined
+        table[students_utils.getClassFromCGPA(level_obj.gpa)[0]].append(line)
+    return 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 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.university.vocabularies import course_levels
+
+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),
+    ]
+
+@implementer(ISessionResultsPresentation)
+class SessionResultsPresentation(Report):
+    data = None
+    session = None
+    level = None
+    faccode = None
+    depcode = None
+
+    def __init__(self, faccode, depcode, session, level, author='System'):
+        super(SessionResultsPresentation, self).__init__(
+            args=[faccode, depcode, session, level], kwargs={'author':author})
+        site = grok.getSite()
+        self.session = academic_sessions_vocab.getTerm(session).title
+        if level == None:
+            self.level = 'All levels'
+        else:
+            self.level = course_levels.getTerm(level).title
+        self.levelcode = level
+        self.sessioncode = session
+        self.faccode = faccode
+        self.depcode = depcode
+        self.factitle = site['faculties'][faccode].longtitle
+        self.deptitle = site['faculties'][faccode][depcode].longtitle
+        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_students(faccode, depcode, session, level)
+
+    def create_pdf(self):
+        creator = getUtility(IPDFCreator)
+        #col_widths = [2*cm, 4*cm, 5*cm, 0.8*cm, 0.8*cm, 0.8*cm, 6*cm, ]
+        pdf_data = [Paragraph('<b>%s</b>' % self.creation_dt_string,
+                              STYLE["Normal"]),
+                    Spacer(1, 12),]
+        pdf_data += [Paragraph('%s<br />%s<br />Level: %s<br />Session: %s' % (
+                    self.factitle, self.deptitle, self.level, self.session),
+                    STYLE["Normal"]),
+                    Spacer(1, 12),]
+        students_utils = getUtility(IStudentsUtils)
+        # Print classes in reverse order
+        for gpa_class in range(len(students_utils.gpa_boundaries)-1,-1,-1):
+            pdf_data.append(Spacer(1, 20))
+            gpa_class_name = students_utils.gpa_boundaries[gpa_class][1]
+            pdf_data += [Paragraph('<strong>%s</strong>' % gpa_class_name,
+                         STYLE["Normal"])]
+            table_data = [('Student Id', 'Matric No.', 'Name')] + self.data[gpa_class]
+            pdf_data += [Table(table_data, style=TABLE_STYLE)]    #, colWidths=col_widths)]
+            gpa_class += 1
+        if self.data[-1]:
+            pdf_data.append(Spacer(1, 20))
+            pdf_data += [
+                Paragraph('<strong>Erroneous Data</strong>', STYLE["Normal"])]
+            table_data = [('Student Id', 'Matric No.', 'Name')] + self.data[-1]
+            pdf_data += [Table(table_data, style=TABLE_STYLE)]
+
+        doc_title = 'Presentation of Session Results'
+        pdf_data.append(Spacer(1, 40))
+        signatures = ['Ag. Head of Department',
+                      'External Examiner', 'Dean of Faculty']
+        signaturetables = get_signature_tables(signatures)
+        pdf_data.append(signaturetables[0])
+
+        pdf = creator.create_pdf(
+            pdf_data, None, doc_title, self.author,
+            'Session Results Presentation %s/%s/%s/%s' % (
+            self.faccode, self.depcode, self.levelcode, self.sessioncode)
+            )
+        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,
+                 session=None, level=None, author=None):
+        result = SessionResultsPresentation(faccode=faccode, depcode=depcode,
+                             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
+grok.templatedir('browser_templates')
+class SessionResultsPresentationGeneratorPage(LevelReportGeneratorPage):
+
+    grok.context(SessionResultsPresentationGenerator)
+    grok.template('levelreportgeneratorpage')
+    label = _('Create session results presentation')
+
+    def update(self, CREATE=None, faccode_depcode=None,
+               session=None, level=None):
+        if level == 'all':
+            level = None
+        super(SessionResultsPresentationGeneratorPage, self).update(
+            CREATE, faccode_depcode ,session, level)
+
+    def _set_level_values(self):
+        vocab_terms = course_levels.by_value.values()
+        self.levels = [('All levels', 'all')] + sorted(
+            [(x.title, x.token) for x in vocab_terms])
+        return
+
+class SessionResultsPresentationPDFView(StudentStatisticsReportPDFView):
+
+    grok.context(ISessionResultsPresentation)
+    grok.name('pdf')
+    grok.require('waeup.Public')
+    prefix = 'SessionResultsPresentation'
+
+    def _filename(self):
+        return 'SessionResultsPresentation_%s_%s_%s_%s_%s.pdf' % (
+            self.context.faccode, self.context.depcode,
+            self.context.sessioncode, self.context.levelcode,
+            self.context.creation_dt_string)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/student_statistics.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/student_statistics.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/student_statistics.py	(revision 10886)
@@ -0,0 +1,241 @@
+## $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.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.interfaces import MessageFactory as _
+from waeup.kofa.reports import IReport
+
+class IStudentStatisticsReport(IReport):
+
+    session = Attribute('Session to report')
+    mode = Attribute('Study modes group to report')
+    creation_dt_string = Attribute('Human readable report creation datetime')
+
+def get_student_stats(session, mode):
+    """Get students in a certain session and study mode.
+
+    Returns a table ordered by faculty code (one per row) and
+    registration state (cols). The result is a 3-tuple representing
+    ((<FACULTY_CODES>), (<STATES>), (<NUM_OF_STUDENTS>)). The
+    (<NUM_OF_STUDENTS>) is an n-tuple with each entry containing the
+    number of students found in that faculty 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',)
+    fac_codes = tuple(sorted([x for x in site['faculties'].keys()],
+                             key=lambda x: x.lower()))
+    fac_codes = fac_codes + (u'Total',)
+    # 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(fac_codes))]
+    mode_groups = getUtility(IKofaUtils).MODE_GROUPS
+    for stud in result:
+        if mode != 'All' and stud.current_mode not in mode_groups[mode]:
+            continue
+        if stud.faccode not in fac_codes:
+            # studs can have a faccode ``None``
+            continue
+        row = fac_codes.index(stud.faccode)
+        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])
+    return (fac_codes, 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
+
+    def __init__(self, session, mode, author='System'):
+        super(StudentStatisticsReport, self).__init__(
+            args=[session, mode], kwargs={'author':author})
+        self.session = academic_sessions_vocab.getTerm(session).title
+        self.mode = mode
+        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)
+
+    def create_pdf(self):
+        creator = getUtility(IPDFCreator, name='landscape')
+        table_data = tbl_data_to_table(*self.data)
+        col_widths = [None,] + [1.6*cm] * len(self.data[1]) + [None,]
+        pdf_data = [Paragraph('<b>%s</b>' % self.creation_dt_string,
+                              STYLE["Normal"]),
+                    Spacer(1, 12),]
+        pdf_data += [
+            Table(table_data, style=TABLE_STYLE, colWidths=col_widths)]
+        doc_title = '%s Students in Session %s' % (self.mode, self.session)
+        pdf = creator.create_pdf(
+            pdf_data, None, doc_title, self.author,
+            'Students in Session %s' % self.session
+            )
+        return pdf
+
+@implementer(IReportGenerator)
+class StudentStatisticsReportGenerator(grok.GlobalUtility):
+
+    title = _('Student Statistics')
+    grok.name('student_stats')
+
+    def generate(self, site, session=None, mode=None, author=None):
+        result = StudentStatisticsReport(session=session, mode=mode, 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
+grok.templatedir('browser_templates')
+class StudentStatisticsReportGeneratorPage(KofaPage):
+
+    grok.context(StudentStatisticsReportGenerator)
+    grok.name('index.html')
+    grok.require('waeup.manageReports')
+
+    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):
+        self.parent_url = self.url(self.context.__parent__)
+        self._set_session_values()
+        self._set_mode_values()
+        if CREATE and session:
+            # create a new report job for students by session
+            container = self.context.__parent__
+            user_id = self.request.principal.id
+            kw = dict(
+                session=int(session),
+                mode=mode)
+            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)' % (
+                ob_class, job_id, self.context.title, session, mode))
+            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
+
+class StudentStatisticsReportPDFView(grok.View):
+
+    grok.context(IStudentStatisticsReport)
+    grok.name('pdf')
+    grok.require('waeup.Public')
+    prefix = ''
+
+    def _filename(self):
+        return 'StudentStatisticsReport_%s_%s_%s.pdf' % (
+            self.context.session, self.context.mode,
+            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()
+        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/tags/0.2/src/waeup/kofa/students/reports/tests/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/tests/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/tests/__init__.py	(revision 10886)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/tests/test_level_report.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/tests/test_level_report.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/tests/test_level_report.py	(revision 10886)
@@ -0,0 +1,110 @@
+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.level_report import (
+    get_students, 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.tests.test_async import FunctionalAsyncTestCase
+from waeup.kofa.browser.tests.test_pdf import samples_dir
+
+class LevelReportTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    def test_iface(self):
+        # ensure we fullfill interface contracts
+        obj = LevelReport('fac1', 'dep1', 2010, 100)
+        verifyClass(ILevelReport, LevelReport)
+        verifyObject(ILevelReport, obj)
+        return
+
+    def test_get_students(self):
+        # we can get a table with one student
+        result = get_students('fac1', 'dep1', 2010, 100)
+        self.assertEqual(result,
+            [(u'1234', u'Bob Tester', 30, 30, 5.0, [], 30, 30, 5.0)])
+        return
+
+    def test_create_pdf(self):
+        report = LevelReport('fac1', 'dep1', 2010, 100)
+        result = report.create_pdf()
+        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')
+        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").value = ['fac1_dep1']
+        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_fac1_dep1_2010_100_' in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 1)
+        job_id = self.app['reports'].running_report_jobs[0][0]
+        # ... 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.level_report.LevelReportGeneratorPage - '
+            'report %s created: Level Report (faculty=fac1, department=dep1, session=2010, level=100)'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - students.reports.level_report.LevelReportPDFView - '
+            'report %s downloaded: LevelReport_fac1_dep1_2010_100_'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - browser.reports.ReportsContainerPage - '
+            'report %s discarded' % job_id in logcontent
+            )
+        return
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/tests/test_session_results_presentation.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/tests/test_session_results_presentation.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/tests/test_session_results_presentation.py	(revision 10886)
@@ -0,0 +1,198 @@
+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 (
+    get_students, 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
+
+class SessionResultsPresentationTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    def test_iface(self):
+        # ensure we fullfill interface contracts
+        obj = SessionResultsPresentation('fac1', 'dep1', 2010, 100)
+        verifyClass(ISessionResultsPresentation, SessionResultsPresentation)
+        verifyObject(ISessionResultsPresentation, obj)
+        return
+
+    def test_get_students(self):
+        result = get_students('fac1', 'dep1', 2010, 100)
+        self.assertEqual(result,
+            [[], [], [], [], [], [(u'K1000000', u'1234', u'Bob Tester')], []])
+        return
+
+    def test_get_students_at_all_levels(self):
+        result = get_students('fac1', 'dep1', 2010)
+        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
+        result = get_students('fac1', 'dep1', 2010)
+        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
+        result = get_students('fac1', 'dep1', 2010)
+        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
+        result = get_students('fac1', 'dep1', 2010)
+        self.assertEqual(result,
+            [[], [], [], [], [], [], []])
+        return
+
+    def test_create_pdf(self):
+        report = SessionResultsPresentation('fac1', 'dep1', 2010, 100)
+        result = report.create_pdf()
+        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_with_two_levels_in_session(self):
+        # Register second level 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
+        self.student['studycourse']['200'] = studylevel
+        report = SessionResultsPresentation('fac1', 'dep1', 2010, None)
+        result = report.create_pdf()
+        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").value = ['fac1_dep1']
+        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_fac1_dep1_2010_100_' in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 1)
+        job_id = self.app['reports'].running_report_jobs[0][0]
+        # ... 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, session=2010, level=100)'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - students.reports.session_results_presentation.SessionResultsPresentationPDFView - '
+            'report %s downloaded: SessionResultsPresentation_fac1_dep1_2010_100_'
+            % 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='all')
+        # 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_fac1_dep1_2010_None_' in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 1)
+        job_id = self.app['reports'].running_report_jobs[0][0]
+        # ... 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, session=2010, level=None)'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - students.reports.session_results_presentation.SessionResultsPresentationPDFView - '
+            'report %s downloaded: SessionResultsPresentation_fac1_dep1_2010_None_'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - browser.reports.ReportsContainerPage - '
+            'report %s discarded' % job_id in logcontent
+            )
+
+        return
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/tests/test_student_statistics.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/tests/test_student_statistics.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/reports/tests/test_student_statistics.py	(revision 10886)
@@ -0,0 +1,148 @@
+# 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_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', 'Total')
+
+    def test_iface(self):
+        # ensure we fullfill interface contracts
+        obj = StudentStatisticsReport(2010, 'Undergraduate Full-Time')
+        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')
+        result2 = get_student_stats(2009, 'Undergraduate Full-Time')
+        self.assertEqual(
+            result1,
+            ((u'fac1', u'Total',),
+             self.states,
+             ((1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
+              (1, 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),)))
+        return
+
+    def test_get_student_stats_session_multiple(self):
+        # we can get a table with several students
+        self.create_cert(u'fac2', u'dept2', u'CERT2')
+        result1 = get_student_stats(2010, 'Undergraduate Full-Time')
+        result2 = get_student_stats(2009, 'Undergraduate Full-Time')
+        self.assertEqual(
+            result1,
+            ((u'fac1', u'fac2', u'Total'),
+             self.states,
+             ((1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (1, 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),)))
+        return
+
+    def test_create_pdf(self):
+        self.create_cert(u'FAC2', u'dept2', u'CERT2')
+        report = StudentStatisticsReport(2010, 'Undergraduate Full-Time')
+        result = report.create_pdf()
+        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
+
+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="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_2004_2005_All_' in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 1)
+        job_id = self.app['reports'].running_report_jobs[0][0]
+        # ... 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)'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - students.reports.student_statistics.StudentStatisticsReportPDFView - '
+            'report %s downloaded: StudentStatisticsReport_2004_2005_All_'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - browser.reports.ReportsContainerPage - '
+            'report %s discarded' % job_id in logcontent
+            )
+        return
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/student.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/student.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/student.py	(revision 10886)
@@ -0,0 +1,579 @@
+## $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 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)
+from waeup.kofa.students.accommodation import StudentAccommodation
+from waeup.kofa.students.export import EXPORTER_NAMES
+from waeup.kofa.students.interfaces import (
+    IStudent, IStudentNavigation, IStudentPersonalEdit, ICSVStudentExporter)
+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)
+
+    temp_password_minutes = 10
+
+    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
+        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)
+        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 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):
+        state = registration_states_vocab.getTermByToken(
+            self.state).title
+        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 before_payment(self):
+        entry_session = getattr(
+            self.get('studycourse', None), 'entry_session', None)
+        non_fresh_states = (PAID, REGISTERED, VALIDATED, RETURNING, )
+        if self.current_session == entry_session 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
+
+    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.
+    """
+    if student.state == CLEARANCE:
+        student.clearance_locked = False
+    else:
+        student.clearance_locked = True
+    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.
+    """
+    for name in 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
+
+    # save files of the student
+    move_student_files(student, del_dir)
+
+    # update CSV files
+    update_student_deletion_csvs(student, del_dir)
+    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/tags/0.2/src/waeup/kofa/students/studycourse.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/studycourse.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/studycourse.py	(revision 10886)
@@ -0,0 +1,171 @@
+## $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,
+    IStudentStudyCourseTranscript)
+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,
+        IStudentStudyCourseTranscript)
+    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 '_' 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.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():
+            tickets_1 = []
+            tickets_2 = []
+            tickets_3 = []
+            tickets = sorted(level_val.values(), key=lambda ticket: ticket.code)
+            for ticket in tickets:
+                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 = round(cgpa/total_credits_counted, 2)
+        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/tags/0.2/src/waeup/kofa/students/studylevel.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/studylevel.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/studylevel.py	(revision 10886)
@@ -0,0 +1,360 @@
+## $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
+from zope.component.interfaces import IFactory
+from zope.catalog.interfaces import ICatalog
+from zope.component import createObject, queryUtility
+from zope.interface import implementedBy
+from waeup.kofa.interfaces import academic_sessions_vocab, VALIDATED
+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
+
+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
+
+def getGradeWeightFromScore(score):
+    if score is None:
+        return (None, None)
+    if score >= 70:
+        return ('A',5)
+    if score >= 60:
+        return ('B',4)
+    if score >= 50:
+        return ('C',3)
+    if score >= 45:
+        return ('D',2)
+    if score >= 40:
+        return ('E',1)
+    return ('F',0)
+
+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__()
+        self.level = None
+        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():
+            total += ticket.credits
+        return total
+
+    @property
+    def getSessionString(self):
+        return academic_sessions_vocab.getTerm(
+            self.level_session).title
+
+    @property
+    def gpa_params_rectified(self):
+        """Calculate corrected corrected level (sesional) 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.score:
+                if ticket.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 = round(credits_weighted/credits_counted, 3)
+        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.score is not None:
+                credits_counted += ticket.credits
+                credits_weighted += ticket.credits * ticket.weight
+        if credits_counted:
+            level_gpa = round(credits_weighted/credits_counted, 3)
+        return level_gpa, credits_counted, credits_weighted
+
+    @property
+    def gpa(self):
+        return self.gpa_params[0]
+
+    @property
+    def passed_params(self):
+        """Determine the number and credits of passed and failed courses.
+
+        This method is used for level reports.
+        """
+        passed = failed = 0
+        courses_failed = []
+        credits_failed = 0
+        credits_passed = 0
+        for ticket in self.values():
+            if ticket.score is not None:
+                if ticket.score < ticket.passmark:
+                    failed += 1
+                    credits_failed += ticket.credits
+                    courses_failed.append(ticket.code)
+                else:
+                    passed += 1
+                    credits_passed += ticket.credits
+        return passed, failed, credits_passed, credits_failed, courses_failed
+
+    @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.
+        """
+        credits_passed = 0
+        total_credits = 0
+        total_credits_counted = 0
+        total_credits_weighted = 0
+        cgpa = 0.0
+        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 = round(total_credits_weighted / total_credits_counted, 3)
+        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):
+        return self.__parent__.__parent__.writeLogMessage(view, message)
+
+    @property
+    def level_title(self):
+        studylevelsource = StudyLevelSource()
+        return studylevelsource.factory.getTitle(self.__parent__, self.level)
+
+    def addCourseTicket(self, ticket, course):
+        """Add a course ticket object.
+        """
+        if not ICourseTicket.providedBy(ticket):
+            raise TypeError(
+                'StudentStudyLeves 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
+                self.addCourseTicket(ticket, val.course)
+        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 referrin 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:
+            return None
+
+    @property
+    def certcode(self):
+        try:
+            return self.__parent__.__parent__.certificate.code
+        except AttributeError:
+            return None
+
+    @property
+    def removable_by_student(self):
+        return not self.mandatory
+
+    @property
+    def editable_by_lecturer(self):
+        cas = grok.getSite()['configuration'].current_academic_session
+        if self.student.state == VALIDATED and self.student.current_session == cas:
+            return True
+        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:
+            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:
+            return None
+
+    @property
+    def grade(self):
+        """Returns the grade calculated from score.
+        """
+        return getGradeWeightFromScore(self.score)[0]
+
+    @property
+    def weight(self):
+        """Returns the weight calculated from score.
+        """
+        return getGradeWeightFromScore(self.score)[1]
+
+    @property
+    def course(self):
+        """Returns the course the ticket is referring to. Returns
+        None if the course has been removed.
+
+        This method is not used in Kofa anymore.
+        """
+        cat = queryUtility(ICatalog, name='courses_catalog')
+        result = cat.searchResults(code=(self.code, self.code))
+        if len(result) != 1:
+            return None
+        return list(result)[0]
+
+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/tags/0.2/src/waeup/kofa/students/tests/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/__init__.py	(revision 10886)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_courseticket_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_courseticket_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_courseticket_data.csv	(revision 10886)
@@ -0,0 +1,10 @@
+reg_number,matric_number,level,code,score,mandatory,level_session
+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/tags/0.2/src/waeup/kofa/students/tests/sample_create_payment_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_create_payment_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_create_payment_data.csv	(revision 10886)
@@ -0,0 +1,51 @@
+p_state,p_category,reg_number,session_id,r_amount_approved,p_item,amount_auth,r_card_num,r_code,creation_date,type
+paid,schoolfee,1,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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,2010,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
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_payment_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_payment_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_payment_data.csv	(revision 10886)
@@ -0,0 +1,5 @@
+matric_number,p_state,p_category,p_id,reg_number,session_id,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
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data.csv	(revision 10886)
@@ -0,0 +1,10 @@
+student_id,firstname,lastname,reg_number,date_of_birth,matric_number,email,phone,sex,state
+X666666,Aaren,Pieri,1,1990-01-02,100000,aa@aa.ng,1234,M,courses validated
+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
+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/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_duplicates.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_duplicates.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_duplicates.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_extascii.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_extascii.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_extascii.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_migration.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_migration.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_migration.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_update.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_update.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_update.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_update2.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_update2.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_update2.csv	(revision 10886)
@@ -0,0 +1,4 @@
+date_of_birth,matric_number,phone
+1970-01-02,100000,
+1970-01-03,100001,XXX
+1970-01-04,100002,<IGNORE>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_update3.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_update3.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_update3.csv	(revision 10886)
@@ -0,0 +1,5 @@
+student_id,reg_number,transition
+X666666,,request_clearance
+Y777777,,return
+,3,return
+,4,register_courses
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_update4.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_update4.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_student_data_update4.csv	(revision 10886)
@@ -0,0 +1,2 @@
+student_id,reg_number,transition,state
+X666666,,request_clearance,returning
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_studycourse_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_studycourse_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_studycourse_data.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/tests/sample_studylevel_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_studylevel_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_studylevel_data.csv	(revision 10886)
@@ -0,0 +1,12 @@
+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
+4,,2008,A,200
+5,,2008,A,200
+6,,2008,A,200
+7,,2008,A,200
+9,,2008,A,200
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_transfer_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_transfer_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_transfer_data.csv	(revision 10886)
@@ -0,0 +1,2 @@
+reg_number,matric_number,certificate,current_session,current_level,entry_mode
+123,,CERT2,2009,300,transfer
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_verdict_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_verdict_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/sample_verdict_data.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/tests/test_authentication.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_authentication.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_authentication.py	(revision 10886)
@@ -0,0 +1,240 @@
+## $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
+
+
+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/tags/0.2/src/waeup/kofa/students/tests/test_batching.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_batching.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_batching.py	(revision 10886)
@@ -0,0 +1,1246 @@
+# -*- 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
+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(',')
+
+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.clearance_locked = False
+        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(2012, 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 = 2012
+        payment.payment_date = datetime.datetime(2012, 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
+        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)
+        self.assertEqual(len(self.app['students']), 10)
+        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))
+        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.
+        container = self.app['students']
+        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
+        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()
+        shutil.rmtree(os.path.dirname(fin_file))
+        self.assertEqual(
+            content,
+            'reg_number,student_id,transition,--ERRORS--\r\n'
+            '<IGNORE>,X666666,request_clearance,Transition not allowed.\r\n'
+            )
+        self.assertEqual(num_warns,1)
+        self.assertEqual(self.app['students']['Y777777'].state,'returning')
+
+    def test_import_update4(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, STUDENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        self.assertRaises(
+            FatalCSVError, self.processor.doImport, self.csv_file_update4,
+            STUDENT_HEADER_FIELDS_UPDATE4, 'update')
+
+    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,,1990-01-06,Kennedy,,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: reg_number\r\n'
+            '2,Aaren,C123456,m,aa@aa.ng,1234,admitted,1990-01-04,Berson,mypw1,100000,matric_number: matric_number\r\n'
+            '1,Frank,F123456,m,aa@aa.ng,1234,,1990-01-06,Meyer,,100000,reg_number: reg_number; matric_number: matric_number\r\n'
+            '3,Uli,A123456,m,aa@aa.ng,1234,,1990-01-07,Schulz,,100002,This object already exists. Skipping.\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)
+
+    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','no valid integer') in errs)
+        errs, inv_errs, conv_dict = self.processor.checkConversion(
+            dict(reg_number='1', level='xyz'))
+        self.assertEqual(len(errs),1)
+        self.assertTrue(('level','no integer') 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,2)
+        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, None)
+        self.assertEqual(studylevel.level, 100)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        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)
+
+    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,2)
+        studylevel = self.processor.getEntry(
+            dict(reg_number='1', level='100'), self.app)
+        self.assertEqual(studylevel.level, 100)
+        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,2)
+
+        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,score,matric_number,--ERRORS--\r\n'
+            '1,COURSE1,,nonsense,,5,,Not all parents do exist yet. Skipping\r\n'
+            '1,NONSENSE,,100,,5,,code: non-existent\r\n'
+            '1,COURSE1,,200,2004,6,,level_session: does not match 2008\r\n'
+            '1,COURSE1,,300,2008,6,,level: does not exist\r\n'
+            '1,COURSE1,,300,2008X,6,,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')
+        shutil.rmtree(os.path.dirname(fin_file))
+        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,score,matric_number,--ERRORS--\r\n'
+            '1,COURSE1,<IGNORE>,nonsense,<IGNORE>,5,<IGNORE>,Cannot update: no such entry\r\n'
+            '1,NONSENSE,<IGNORE>,100,<IGNORE>,5,<IGNORE>,code: non-existent\r\n'
+            '1,COURSE1,<IGNORE>,200,2004,6,<IGNORE>,level_session: does not match 2008\r\n'
+            '1,COURSE1,<IGNORE>,300,2008,6,<IGNORE>,level: does not exist\r\n'
+            '1,COURSE1,<IGNORE>,300,2008X,6,<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)
+
+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'
+        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')
+
+    def test_checkConversion(self):
+        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)
+        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)
+
+    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 - Student Payment 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_id=p1266236341955, r_code=00, r_amount_approved=19500.0, '
+            'p_state=paid'
+            in logcontent)
+        self.assertTrue(
+            'INFO - system - Student Payment 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_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 must be the same.
+        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,0)
+        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, 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,0)
+        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_wo_pid(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file2, PAYMENT_CREATE_HEADER_FIELDS,'create')
+        self.assertEqual(num_warns,0)
+        shutil.rmtree(os.path.dirname(fin_file))
+        self.assertEqual(len(self.app['students']['X666666']['payments']), 50)
+
+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, None)
+        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')
+        #content = open(fail_file).read()
+        #import pdb; pdb.set_trace()
+        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/tags/0.2/src/waeup/kofa/students/tests/test_browser.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_browser.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_browser.py	(revision 10886)
@@ -0,0 +1,3516 @@
+## $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 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.students.interfaces import IStudentsUtils
+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
+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
+
+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')
+
+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
+        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 student 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 record").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 record").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
+
+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')
+        self.assertEqual(self.browser.url, self.acco_path)
+        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_manage_contact_student(self):
+        # Managers can contact student
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.student.email = None
+        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(
+            '<a target="image" href="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)
+        # Reuploading a file which is bigger than 150k will raise an error
+        self.browser.open(self.manage_clearance_path)
+        # An image > 150K
+        big_image = StringIO(open(SAMPLE_IMAGE, 'rb').read() * 75)
+        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)
+        # 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 delete files
+        self.browser.getControl(name='delete_birthcertificateupload').click()
+        self.assertTrue(
+            'birth_certificate deleted' 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 extension 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(
+            '<img align="middle" height="125px" src="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')
+
+    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(
+            '...: Study Level 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 - removed: COURSE1 at 100' 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(
+            '...Total credits exceed 50...', 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.565)
+        # 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(
+            '...: Study Level 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, []))
+        # 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.261, 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.261, 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
+
+        # 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.assertTrue(len(results), 1)
+        self.assertTrue(results[0] is self.student['payments'][value])
+
+        # Managers can approve the payment
+        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 ...
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.browser.OnlinePaymentApprovePage '
+            '- 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 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
+        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_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 previous 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()
+
+        # 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.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')
+
+    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.getLink("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.getLink("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.getLink("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.getLink("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.browser.BedTicketRelocationPage '
+            '- 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')
+        student = self.app['students'][self.student_id]
+        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()
+        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()
+        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 test_handle_clearance_by_co(self):
+        # Create clearance officer
+        self.app['users'].addUser('mrclear', 'mrclearsecret')
+        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
+        department = self.app['faculties']['fac1']['dep1']
+        prmlocal = IPrincipalRoleManager(department)
+        prmlocal.assignRoleToPrincipal('waeup.local.ClearanceOfficer', 'mrclear')
+        IWorkflowState(self.student).setState('clearance started')
+        # 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 = 'mrclearsecret'
+        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)
+        #self.assertMatches(
+        #    '...<div>Students 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(
+            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
+        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)
+        # 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)
+        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,
+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.'
+              '%0A&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.\n')
+        # ... 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.<br>\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)
+
+    def test_handle_courses_by_ca(self):
+        # Create course adviser
+        self.app['users'].addUser('mrsadvise', 'mrsadvisesecret')
+        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 = 'mrsadvisesecret'
+        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 request has been rejected'
+            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_handle_courses_by_lecturer(self):
+        # Create course lecturer
+        self.app['users'].addUser('mrslecturer', 'mrslecturersecret')
+        self.app['users']['mrslecturer'].email = 'mrslecturer@foo.ng'
+        self.app['users']['mrslecturer'].title = u'Mercedes Benz'
+        # Assign local Lecturer role for a certificate
+        course = self.app['faculties']['fac1']['dep1'].courses['COURSE1']
+        prmlocal = IPrincipalRoleManager(course)
+        prmlocal.assignRoleToPrincipal('waeup.local.Lecturer', 'mrslecturer')
+        # Login as lecturer
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrslecturer'
+        self.browser.getControl(name="form.password").value = 'mrslecturersecret'
+        self.browser.getControl("Login").click()
+        self.assertMatches('...You logged in...', self.browser.contents)
+        # CO can see her roles
+        self.browser.getLink("My Roles").click()
+        self.assertMatches(
+            '...<div>Academics Officer (view only)</div>...',
+            self.browser.contents)
+        # But not her local role ...
+        self.assertFalse('Lecturer' in self.browser.contents)
+        # ... because we forgot to notify the course that the local role
+        # has changed
+        notify(LocalRoleSetEvent(
+            course, 'waeup.local.Lecturer', 'mrslecturer', granted=True))
+        self.browser.open('http://localhost/app/users/mrslecturer/my_roles')
+        self.assertTrue('Lecturer' in self.browser.contents)
+        self.assertMatches(
+            '...<a href="http://localhost/app/faculties/fac1/dep1/courses/COURSE1">...',
+            self.browser.contents)
+        # The lecturer can go to her course
+        self.browser.getLink(
+            "http://localhost/app/faculties/fac1/dep1/courses/COURSE1").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('No student found.' in self.browser.contents)
+        # No student in course so far
+        self.assertFalse(self.student_id in self.browser.contents)
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = 100
+        studylevel.level_session = 2004
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, studylevel)
+        # Now the student has registered the course and can
+        # be seen by the lecturer.
+        self.browser.open("http://localhost/app/faculties/fac1/dep1/courses/COURSE1/students")
+        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 course ticket can be linked with the course.
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].course,
+            self.course)
+        # Lecturer can neither 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)
+        # Course results can be batch edited via the edit_courses view
+        self.app['faculties']['fac1']['dep1'].score_editing_disabled = True
+        self.browser.open("http://localhost/app/faculties/fac1/dep1/courses/COURSE1")
+        self.browser.getLink("Update scores").click()
+        self.assertTrue('Score editing disabled' in self.browser.contents)
+        self.app['faculties']['fac1']['dep1'].score_editing_disabled = False
+        self.browser.getLink("Update scores").click()
+        self.assertTrue('Current academic session not set' in self.browser.contents)
+        self.app['configuration'].current_academic_session = 2004
+        self.browser.getLink("Update scores").click()
+        self.assertFalse(
+            '<input type="text" name="scores" class="span1" />'
+            in self.browser.contents)
+        IWorkflowState(self.student).setState('courses validated')
+        # Student must be in state 'courses validated'
+        self.browser.open(
+            "http://localhost/app/faculties/fac1/dep1/courses/COURSE1/edit_scores")
+        self.assertTrue(
+            '<input type="text" name="scores" class="span1" />'
+            in self.browser.contents)
+        self.browser.getControl(name="scores", index=0).value = '55'
+        self.browser.getControl("Update scores").click()
+        # New score has been set
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].score, 55)
+        # Score editing has been logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('mrslecturer - students.browser.EditScoresPage - '
+                        'K1000000 100/COURSE1 score updated (55)' in logcontent)
+        # Non-integer scores won't be accepted
+        self.browser.open(
+            "http://localhost/app/faculties/fac1/dep1/courses/COURSE1/edit_scores")
+        self.assertTrue('value="55" />' in self.browser.contents)
+        self.browser.getControl(name="scores", index=0).value = 'abc'
+        self.browser.getControl("Update scores").click()
+        self.assertTrue('Error: Score(s) of Anna Tester have not be updated'
+            in self.browser.contents)
+        # Scores can be removed
+        self.browser.open(
+            "http://localhost/app/faculties/fac1/dep1/courses/COURSE1/edit_scores")
+        self.browser.getControl(name="scores", index=0).value = ''
+        self.browser.getControl("Update scores").click()
+        self.assertEqual(
+            self.student['studycourse']['100']['COURSE1'].score, None)
+        logcontent = open(logfile).read()
+        self.assertTrue('mrslecturer - students.browser.EditScoresPage - '
+                        'K1000000 100/COURSE1 score updated (None)' in logcontent)
+
+    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.student.clearance_locked = False
+        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)
+        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', 'mrmanagersecret')
+        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 = 'mrmanagersecret'
+        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.StudentDeactivatePage - '
+                        'K1000000 - account deactivated' in logcontent)
+        self.assertTrue('zope.mgr - students.browser.StudentActivatePage - '
+                        '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', 'mrofficersecret')
+        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 = 'mrofficersecret'
+        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">My Data</a>...',
+            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.57)
+        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_request(self):
+        IWorkflowState(self.student).setState('transcript requested')
+        notify(grok.ObjectModifiedEvent(self.student))
+        self.student.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 transcript officer
+        self.app['users'].addUser('mrtranscript', 'mrtranscriptsecret')
+        self.app['users']['mrtranscript'].email = 'mrtranscript@foo.ng'
+        self.app['users']['mrtranscript'].title = 'Ruth Gordon'
+        prmglobal = IPrincipalRoleManager(self.app)
+        prmglobal.assignRoleToPrincipal('waeup.TranscriptOfficer', 'mrtranscript')
+        # 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 = 'mrtranscriptsecret'
+        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 'transcripr requested'
+        self.browser.open(self.container_path)
+        self.browser.getControl(name="searchtype").value = ['transcript']
+        self.browser.getControl("Find student(s)").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        self.browser.getLink("K1000000").click()
+        self.browser.getLink("Manage transcript request").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 mark as processed").click()
+        self.assertTrue(
+            'UTC mrtranscript wrote:\n\nHello,\nYour transcript has '
+            'been sent to the address provided.\n\n'
+            in self.student.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.StudentTranscriptRequestProcessFormPage - '
+            'K1000000 - comment: Hello,<br>'
+            'Your transcript has been sent to the address provided'
+            in logcontent)
+
+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')
+        # 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_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-message 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-message 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_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(
+            '<img align="middle" height="125px" src="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')
+        # 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.email = None
+        # Uups, we forgot to fill the email fields
+        self.browser.getControl("Start clearance").click()
+        self.assertMatches('...Not all required fields filled...',
+                           self.browser.contents)
+        self.browser.open(self.student_path + '/edit_base')
+        self.browser.getControl(name="form.email").value = 'aa@aa.ng'
+        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(
+            '<a target="image" href="birth_certificate">Birth Certificate Scan</a>'
+            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.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(
+            '...Total credits exceed 50...', 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)
+        # 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')
+        # 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 of 50 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 - removed: COURSE1 at 200' 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 - 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')
+        # 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_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()
+        # A level with one course ticket was created
+        self.assertEqual(self.student['studycourse']['999'].number_of_tickets, 0)
+        self.browser.getLink("999").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('...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.student.clearance_locked = False
+        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)
+
+        # 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):
+        # 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 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.getLink("Book accommodation").click()
+        self.assertMatches('...Outside booking period: ...',
+                           self.browser.contents)
+        self.app['hostels'].enddate = datetime.now(pytz.utc) + timedelta(days=10)
+        # ... or student is not the an allowed state ...
+        self.browser.getLink("Book accommodation").click()
+        self.assertMatches('...You are in the wrong...',
+                           self.browser.contents)
+        IWorkflowInfo(self.student).fireTransition('admit')
+        self.browser.getLink("Book accommodation").click()
+        self.assertMatches('...Activation Code:...',
+                           self.browser.contents)
+        # Student can't used 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
+        self.browser.getControl("Create bed ticket").click()
+        self.assertMatches('...You are not the owner of this access code...',
+                           self.browser.contents)
+        ac.owner = self.student_id
+        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
+        bed = self.app['hostels']['hall-1']['hall-1_A_101_A']
+        self.assertTrue(bed.owner == self.student_id)
+
+        # BedTicketAddPage is now blocked
+        self.browser.getLink("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')
+
+        # 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 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)
+        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_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_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.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)
+
+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.firstname").value = 'Anna'
+        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 firstname verification failed.
+        # It seems that the record doesn't exist.
+        self.browser.open('http://localhost/app/requestpw')
+        self.browser.getControl(name="form.firstname").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 firstname we can't register if a
+        # password has been set and used.
+        self.browser.getControl(name="form.firstname").value = 'Anna'
+        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 firstname field, used for verification, is not case-sensitive.
+        self.browser.getControl(name="form.firstname").value = 'aNNa'
+        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.firstname").value = 'aNNa'
+        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 test_faculties_export(self):
+        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("Configure new export").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("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.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), 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
+            )
+
+    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("Configure new export").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']
+        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, dep1, 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 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("Configure new export").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, CERT1), 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_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("Configure new export").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_coursetickets(self):
+        # We add study level 100 to the student's studycourse
+        studylevel = StudentStudyLevel()
+        studylevel.level = 100
+        studylevel.level_session = 2004
+        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("Configure new export").click()
+        self.browser.getControl(name="exporter").value = ['coursetickets']
+        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_coursetickets_%s.csv' % job_id in
+            self.browser.headers['content-disposition'])
+        # ... and contains the course ticket COURSE1
+        self.assertEqual(self.browser.contents,
+            'automatic,carry_over,code,credits,dcode,fcode,level,'
+            'level_session,mandatory,passmark,score,semester,title,'
+            'student_id,certcode\r\n1,0,COURSE1,10,dep1,fac1,100,2004,1,40,,1,'
+            'Unnamed Course,K1000000,CERT1\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: coursetickets (2004, 100, COURSE1), job_id=%s'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - students.browser.ExportJobContainerDownload '
+            '- downloaded: WAeUP.Kofa_coursetickets_%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', 'mrdepartmentsecret')
+        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 = 'mrdepartmentsecret'
+        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("Configure new export").click()
+        # Only the paymentsoverview exporter is available for department officers
+        self.assertFalse('<option value="students">' in self.browser.contents)
+        self.assertTrue(
+            '<option value="paymentsoverview">' in self.browser.contents)
+        self.browser.getControl(name="exporter").value = ['paymentsoverview']
+        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)
+        #job_id = self.app['datacenter'].running_exports[0][0]
+        job_id = 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):
+        # Create bursary officer
+        self.app['users'].addUser('mrbursary', 'mrbursarysecret')
+        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 = 'mrbursarysecret'
+        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("Configure new export").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("Configure new export").click()
+        self.assertFalse('<option value="students">' in self.browser.contents)
+        self.assertTrue('<option value="bursary">' in self.browser.contents)
+        # Thew job can be discarded
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        #job_id = self.app['datacenter'].running_exports[0][0]
+        job_id = 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)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_catalog.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_catalog.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_catalog.py	(revision 10886)
@@ -0,0 +1,285 @@
+## $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 = StudentStudyLevel()
+        studylevel.level = 100
+        studylevel.level_session = 2010
+        self.student['studycourse']['100'] = studylevel
+        ticket = CourseTicket()
+        ticket.code = 'Course1'
+        ticket.credits = 30
+        ticket.score = 88
+        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.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/tags/0.2/src/waeup/kofa/students/tests/test_container.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_container.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_container.py	(revision 10886)
@@ -0,0 +1,107 @@
+## $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()
+        self.assertRaises(
+            NotImplementedError, container.archive)
+        self.assertRaises(
+            NotImplementedError, container.clear)
+        # 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/tags/0.2/src/waeup/kofa/students/tests/test_dynamicroles.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_dynamicroles.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_dynamicroles.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/tests/test_export.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_export.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_export.py	(revision 10886)
@@ -0,0 +1,1000 @@
+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.students.export import (
+    StudentsExporter, StudentStudyCourseExporter, StudentStudyLevelExporter,
+    CourseTicketExporter, StudentPaymentsExporter, BedTicketsExporter,
+    StudentPaymentsOverviewExporter, StudentStudyLevelsOverviewExporter,
+    ComboCardDataExporter, DataForBursaryExporter,
+    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 - 9, 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 StudentsExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    std_csv_entry = (
+        'my adm code,0,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(StudentsExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = StudentsExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, StudentsExporter)
+        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 = StudentsExporter()
+        exporter.export([self.student], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'adm_code,clearance_locked,clr_code,date_of_birth,email,'
+            'employer,firstname,lastname,matric_number,middlename,'
+            'nationality,officer_comment,perm_address,personal_updated,'
+            'phone,reg_number,sex,student_id,suspended,suspended_comment,'
+            'transcript_comment,password,state,history,certcode,is_postgrad,'
+            'current_level,current_session\r\n'
+            'my adm code,0,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_all(self):
+        # we can really export students
+        # set values we can expect in export file
+        self.setup_student(self.student)
+        exporter = StudentsExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'adm_code,clearance_locked,clr_code,date_of_birth,email,'
+            'employer,firstname,lastname,matric_number,middlename,'
+            'nationality,officer_comment,perm_address,personal_updated,'
+            'phone,reg_number,sex,student_id,suspended,suspended_comment,'
+            'transcript_comment,password,state,history,certcode,'
+            'is_postgrad,current_level,current_session\r\n'
+            'my adm code,0,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 = StudentsExporter()
+        exporter.export_student(self.student, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertTrue(
+            'adm_code,clearance_locked,clr_code,date_of_birth,email,'
+            'employer,firstname,lastname,matric_number,middlename,'
+            'nationality,officer_comment,perm_address,personal_updated,'
+            'phone,reg_number,sex,student_id,suspended,suspended_comment,'
+            'transcript_comment,password,state,history,certcode,'
+            'is_postgrad,current_level,current_session\r\n'
+            'my adm code,0,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 = StudentsExporter()
+
+        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 can be both a string ...
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=None, 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()
+        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.assertTrue(self.std_csv_entry not in result4)
+        self.assertTrue(self.std_csv_entry not in result5)
+        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 = StudentsExporter()
+        # 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 = StudentsExporter()
+
+        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
+
+
+
+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,'
+            'validated_by,validation_date,'
+            'student_id,number_of_tickets,certcode\r\n'
+            '0.0,,,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,'
+            'validated_by,validation_date,'
+            'student_id,number_of_tickets,certcode\r\n'
+            '0.0,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,'
+            'validated_by,validation_date,'
+            'student_id,number_of_tickets,certcode\r\n'
+            '0.0,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,'
+            'validated_by,validation_date,'
+            'student_id,number_of_tickets,certcode\r\n'
+            '0.0,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, current_level=200)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'gpa,level,level_session,level_verdict,total_credits,'
+            'validated_by,validation_date,'
+            'student_id,number_of_tickets,certcode\r\n'
+            '0.0,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,credits,dcode,fcode,level,level_session,'
+            'mandatory,passmark,score,semester,title,student_id,certcode\r\n'
+            '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,credits,dcode,fcode,level,level_session,'
+            'mandatory,passmark,score,semester,title,student_id,certcode\r\n'
+            '1,1,CRS1,100,DEP1,FAC1,100,2012,0,100,,2,Course 1,A111111,CERT1\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,credits,dcode,fcode,level,level_session,'
+            'mandatory,passmark,score,semester,title,student_id,certcode\r\n'
+            '1,1,CRS1,100,DEP1,FAC1,100,2012,0,100,,2,Course 1,A111111,CERT1\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,credits,dcode,fcode,level,level_session,'
+            'mandatory,passmark,score,semester,title,student_id,certcode\r\n'
+            '1,1,CRS1,100,DEP1,FAC1,100,2012,0,100,,2,Course 1,A111111,CERT1\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, current_level=200)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'automatic,carry_over,code,credits,dcode,fcode,level,level_session,'
+            'mandatory,passmark,score,semester,title,student_id,certcode\r\n'
+            '1,1,CRS1,100,DEP1,FAC1,100,2012,0,100,,2,Course 1,A111111,CERT1\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,credits,dcode,fcode,level,level_session,'
+            'mandatory,passmark,score,semester,title,student_id,certcode\r\n'
+            '1,1,CRS1,100,DEP1,FAC1,100,2012,0,100,,2,Course 1,A111111,CERT1\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,credits,dcode,fcode,level,level_session,'
+            'mandatory,passmark,score,semester,title,student_id,certcode\r\n'
+            )
+        return
+
+class StudentPaymentsExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentPaymentsExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = StudentPaymentsExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, StudentPaymentsExporter)
+        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 = StudentPaymentsExporter()
+        exporter.export([payment], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,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 = StudentPaymentsExporter()
+        exporter.export([payment], self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,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,2012-04-01 13:12:01,schoolfee,1,my-id,'
+            'p-item,100,2012,paid,2012-04-01 14:12:01,12.12,'
+            'r-code,,A111111,created,2012\r\n'
+            )
+        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 = StudentPaymentsExporter()
+        exporter.export_all(self.app, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,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,2012-04-01 13:12:01,schoolfee,1,my-id,'
+            'p-item,100,2012,paid,2012-04-01 14:12:01,12.12,'
+            'r-code,,A111111,created,2012\r\n'
+            )
+        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 = StudentPaymentsExporter()
+        exporter.export_student(self.student, self.outfile)
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,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,2012-04-01 13:12:01,schoolfee,1,my-id,'
+            'p-item,100,2012,paid,2012-04-01 14:12:01,12.12,'
+            'r-code,,A111111,created,2012\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 = StudentPaymentsExporter()
+        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_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,2012-04-01 13:12:01,schoolfee,1,my-id,'
+            'p-item,100,2012,paid,2012-04-01 14:12:01,12.12,'
+            'r-code,,A111111,created,2012\r\n'
+            )
+        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_current,p_id,p_item,'
+            'p_level,p_session,p_state,payment_date,r_amount_approved,r_code,'
+            'r_desc,student_id,matric_number,firstname,middlename,lastname,'
+            'state,current_session,entry_session,entry_mode,faccode,depcode\r\n'
+
+            '666,12.12,2012-04-01 13:12:01,schoolfee,1,my-id,p-item,100,2012,'
+            'paid,2012-04-01 14:12:01,12.12,r-code,,A111111,234,'
+            'Anna,M.,Tester,created,2012,2010,ug_ft,NA,NA\r\n'
+            )
+        return
+
+class BedTicketsExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(BedTicketsExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = BedTicketsExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, BedTicketsExporter)
+        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 = BedTicketsExporter()
+        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 = BedTicketsExporter()
+        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 = BedTicketsExporter()
+        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 = BedTicketsExporter()
+        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 = BedTicketsExporter()
+        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 StudentPaymentsOverviewExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentPaymentsOverviewExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        obj = StudentPaymentsOverviewExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, StudentPaymentsOverviewExporter)
+        return
+
+    def test_get_as_utility(self):
+        # we can get a payments exporter as utility
+        result = queryUtility(ICSVExporter, name="paymentsoverview")
+        self.assertTrue(result is not None)
+        return
+
+    def test_export(self):
+        self.setup_student(self.student)
+        exporter = StudentPaymentsOverviewExporter()
+        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,'
+            '%s\r\n'
+            'A111111,234,Anna M. Tester,created,CERT1,NA,NA,0,200,2012,ug_ft,'
+            % 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 = StudentOnlinePayment()
+        payment.p_id = 'my-id'
+        payment.p_session = 2009
+        payment.amount_auth = 13.13
+        payment.p_state = u'paid'
+        payment.p_category = u'schoolfee'
+        self.student['payments']['my-2ndpayment'] = payment
+        exporter = StudentPaymentsOverviewExporter()
+        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,'
+            '%s\r\nA111111,234,Anna M. Tester,created,CERT1,NA,NA,0,'
+            '200,2012,ug_ft' % year_range_str in result
+            )
+        return
+
+class StudentStudyLevelsOverviewExporterTest(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentStudyLevelsOverviewExporterTest, self).setUp()
+        self.setup_for_export()
+        return
+
+    def test_ifaces(self):
+        obj = StudentStudyLevelsOverviewExporter()
+        verifyObject(ICSVStudentExporter, obj)
+        verifyClass(ICSVStudentExporter, StudentStudyLevelsOverviewExporter)
+        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 = StudentStudyLevelsOverviewExporter()
+        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,'
+             '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 = StudentStudyLevelsOverviewExporter()
+        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,'
+            '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
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_move.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_move.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_move.py	(revision 10886)
@@ -0,0 +1,64 @@
+## $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.department import Department
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+
+class MoveObjectsInFaculties(StudentsFullSetup):
+
+    def test_move_certificate(self):
+        self.assertEqual(
+            self.app['faculties']['fac1']['dep1'].certificates['CERT1'],
+            self.certificate)
+        self.app['faculties']['fac1']['dep2'] = Department(code=u'dep2')
+        self.certificate.moveCertificate('fac1', 'dep2')
+        self.assertEqual(
+            self.app['faculties']['fac1']['dep2'].certificates['CERT1'],
+            self.certificate)
+        self.assertEqual(
+            [key for key in self.app['faculties']['fac1'][
+                'dep2'].certificates['CERT1'].keys()],
+            [u'COURSE1_100'])
+        self.assertEqual(
+            self.student['studycourse'].certificate,
+            self.certificate)
+
+        # 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] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.certificate
+
+        # 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]
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('INFO - system - K1000000 - Certificate moved'
+            in logcontent)
+
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_payment.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_payment.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_payment.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/students/tests/test_student.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_student.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_student.py	(revision 10886)
@@ -0,0 +1,393 @@
+## $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 waeup.kofa.interfaces import IExtFileStore, IFileStoreNameChooser
+from waeup.kofa.students.export import EXPORTER_NAMES
+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)
+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.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
+
+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
+        for name in 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/tags/0.2/src/waeup/kofa/students/tests/test_utils.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_utils.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_utils.py	(revision 10886)
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+import unittest
+from waeup.kofa.students.utils import formatted_text
+
+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
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_vocabularies.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_vocabularies.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_vocabularies.py	(revision 10886)
@@ -0,0 +1,124 @@
+## $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, [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), 18)
+        # 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),
+            '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 to pre-studies
+        # course
+        studycourse.certificate.start_level = 10
+        studycourse.certificate.end_level = 10
+        values = studylevelsource.getValues(studycourse)
+        self.assertEqual(values, [10])
+        titles = [studylevelsource.getTitle(studycourse, value)
+            for value in values]
+        self.assertEqual(len(titles), 1)
+        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, [10, 100, 110, 120,
+            200, 210, 220, 300, 310, 320])
+        titles = [studylevelsource.getTitle(studycourse, value)
+            for value in values]
+        self.assertEqual(len(titles), 10)
+        # Repeating Pre-Studies level does not exist and raises a KeyError
+        # when trying to get its title
+        self.assertRaises(KeyError,studylevelsource.getTitle,studycourse,20)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_webservices.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_webservices.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/tests/test_webservices.py	(revision 10886)
@@ -0,0 +1,377 @@
+# 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
+from waeup.kofa.testing import FunctionalLayer
+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 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
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/utils.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/utils.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/utils.py	(revision 10886)
@@ -0,0 +1,855 @@
+## $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 student section.
+"""
+import grok
+from time import time
+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 zope.schema.interfaces import ConstraintNotSatisfied
+from zope.component import getUtility, createObject
+from zope.formlib.form import setUpEditWidgets
+from zope.i18n import translate
+from waeup.kofa.interfaces import (
+    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED,
+    academic_sessions_vocab)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.students.interfaces import IStudentsUtils
+from waeup.kofa.students.workflow import ADMITTED
+from waeup.kofa.browser.pdf import (
+    ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE, 
+    get_signature_tables)
+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 = ''
+    # 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, omit_fields=(), lang='en'):
+    """Render student table for an existing frame.
+    """
+    width, height = A4
+    set_up_widgets(studentview, ignore_request=True)
+    data_left = []
+    data_right = []
+    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_right.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_right.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_right.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_right.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_right.append([f_label,f_text])
+        if not 'current_mode' in omit_fields:
+            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
+            sm = studymodes_dict[studentview.context[
+                'studycourse'].certificate.study_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_right.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['studycourse'].entry_session
+            entry_session = academic_sessions_vocab.getTerm(entry_session).title
+            f_text = formatted_text(entry_session)
+            f_text = Paragraph(f_text, ENTRY1_STYLE)
+            data_right.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_right.append([f_label,f_text])
+
+    table_left = Table(data_left,style=SLIP_STYLE)
+    table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm])
+    table = Table([[table_left, 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()
+    for level in levels_data:
+        level_obj = level['level']
+        tickets = level['tickets_1'] + level['tickets_2'] + level['tickets_3']
+        headerline = []
+        tabledata = []
+        subheader = '%s %s, %s %s' % (
+            trans(_('Session'), lang),
+            view.session_dict[level_obj.level_session],
+            trans(_('Level'), lang),
+            view.level_dict[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 = '%s: %s' % (trans('Sessional GPA (rectified)', lang), level['sgpa'])
+        data.append(Paragraph(sgpa, 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.students.viewlets 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 setPaymentDetails(self, category, student,
+            previous_session, previous_level):
+        """Create Payment object 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:
+                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:
+                # Should not happen because this is already checked
+                # in the browser module, but anyway ...
+                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+                p_item = trans(_('no bed allocated'), portal_language)
+        elif category == 'transcript':
+            amount = academic_session.transcript_fee
+        if amount in (0.0, None):
+            return _('Amount could not be determined.'), None
+        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 _('This type of payment has already been made.'), 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 setBalanceDetails(self, category, student,
+            balance_session, balance_level, balance_amount):
+        """Create Payment object and set the payment data of a student for.
+
+        """
+        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
+        for key in student['payments'].keys():
+            ticket = student['payments'][key]
+            if ticket.p_state == 'paid' and\
+               ticket.p_category == 'balance' and \
+               ticket.p_item == p_item and \
+               ticket.p_session == p_session:
+                  return _('This type of payment has already been made.'), 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 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 selectBed(self, available_beds):
+        """Select a bed from a list of available beds.
+
+        In the base configuration we select the first bed found,
+        but can also randomize the selection if we like.
+        """
+        return available_beds[0]
+
+    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,):
+        """Render pdf admission letter.
+        """
+        if student is None:
+            return
+        style = getSampleStyleSheet()
+        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)
+        data.append(Paragraph(html, NOTE_STYLE))
+        data.append(Spacer(1, 20))
+
+        # Student data
+        data.append(render_student_data(view, omit_fields, lang=portal_language))
+
+        # 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)
+        return pdf_stream
+
+    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.
+        """
+        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, omit_fields, lang=portal_language))
+
+        # 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.startswith('clearance'):
+            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]:
+                    #data.append(PageBreak())
+                    #data.append(Spacer(1, 20))
+                    data.append(Paragraph(view.tabletitle[i], 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))
+        return pdf_stream
+
+    gpa_boundaries = ((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):
+        if gpa < self.gpa_boundaries[0][0]:
+            return 0, self.gpa_boundaries[0][1]
+        if gpa < self.gpa_boundaries[1][0]:
+            return 1, self.gpa_boundaries[1][1]
+        if gpa < self.gpa_boundaries[2][0]:
+            return 2, self.gpa_boundaries[2][1]
+        if gpa < self.gpa_boundaries[3][0]:
+            return 3, self.gpa_boundaries[3][1]
+        if gpa < self.gpa_boundaries[4][0]:
+            return 4, self.gpa_boundaries[4][1]
+        if gpa <= self.gpa_boundaries[5][0]:
+            return 5, self.gpa_boundaries[5][1]
+        return 'N/A'
+
+    def renderPDFTranscript(self, view, filename='transcript.pdf',
+                  student=None,
+                  studentview=None,
+                  note=None, signatures=None, sigs_in_footer=(),
+                  show_scans=True, topMargin=1.5,
+                  omit_fields=(),
+                  tableheader=None):
+        """Render pdf slips for transcript.
+        """
+        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, omit_fields, lang=portal_language))
+
+
+        transcript_data = view.context.getTranscriptData()
+        levels_data = transcript_data[0]
+        gpa = transcript_data[1]
+
+        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)
+        f_text = formatted_text('%s (%s)' % (gpa, self.getClassFromCGPA(gpa)[1]))
+        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])
+
+        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))
+        return pdf_stream
+
+    def maxCredits(self, studylevel):
+        """Return maximum credits.
+
+        In some universities maximum credits is not constant, it
+        depends on the student's study level.
+        """
+        return 50
+
+    def maxCreditsExceeded(self, studylevel, course):
+        max_credits = self.maxCredits(studylevel)
+        if max_credits and \
+            studylevel.total_credits + course.credits > max_credits:
+            return max_credits
+        return 0
+
+    def getBedCoordinates(self, bedticket):
+        """Return bed coordinates.
+
+        This method can be used to customize the display_coordinates
+        property method.
+        """
+        return bedticket.bed_coordinates
+
+    VERDICTS_DICT = {
+        '0': _('(not yet)'),
+        'A': 'Successful student',
+        'B': 'Student with carryover courses',
+        'C': 'Student on probation',
+        }
+
+    SEPARATORS_DICT = {
+        }
+
+    SKIP_UPLOAD_VIEWLETS = ()
+
+    PWCHANGE_STATES = (ADMITTED,)
+
+    #: 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/tags/0.2/src/waeup/kofa/students/viewlets.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/viewlets.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/viewlets.py	(revision 10886)
@@ -0,0 +1,1036 @@
+3## $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 zope.component import getUtility
+from zope.interface import Interface
+from zope.i18n import translate
+from waeup.kofa.interfaces import (
+    IKofaObject, IExtFileStore, IFileStoreNameChooser)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.utils.helpers import string_from_bytes, file_size
+from waeup.kofa.browser import DEFAULT_IMAGE_PATH
+from waeup.kofa.browser.viewlets import (
+    PrimaryNavTab, ManageActionButton, AddActionButton)
+from waeup.kofa.browser.layout import (
+    default_primary_nav_template, default_filedisplay_template,
+    default_fileupload_template)
+from waeup.kofa.students.workflow import (
+    ADMITTED, PAID, REQUESTED, RETURNING, CLEARED, REGISTERED,
+    VALIDATED, GRADUATED, TRANSCRIPT)
+from waeup.kofa.students.browser import (
+    clearance_disabled_message,
+    StudentClearanceManageFormPage,
+    StudentBaseManageFormPage, StudentFilesUploadPage,
+    ExportPDFClearanceSlipPage, StudentsContainerPage,
+    StudentsContainerManagePage, StudentBaseDisplayFormPage,
+    StudentClearanceDisplayFormPage, StudentPersonalDisplayFormPage,
+    StudyCourseDisplayFormPage, StudyLevelDisplayFormPage,
+    CourseTicketDisplayFormPage, OnlinePaymentDisplayFormPage,
+    AccommodationManageFormPage, BedTicketDisplayFormPage,
+    StudentClearanceEditFormPage, StudentPersonalEditFormPage,
+    PaymentsManageFormPage, StudyCourseTranscriptPage)
+from waeup.kofa.students.interfaces import (
+    IStudentsContainer, IStudent, IStudentStudyCourse, IStudentAccommodation,
+    IStudentStudyLevel, ICourseTicket, IStudentOnlinePayment, IBedTicket,
+    IStudentPaymentsContainer, IStudentsUtils
+    )
+from waeup.kofa.utils.helpers import get_fileformat
+
+grok.context(IKofaObject) # Make IKofaObject the default context
+grok.templatedir('browser_templates')
+
+ALLOWED_FILE_EXTENSIONS = ('jpg', 'png', 'pdf', 'tif')
+
+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.handleAccommodation')
+    link = 'accommodation'
+    text = _(u'Accommodation')
+
+class StudentManageHistoryLink(StudentManageLink):
+    grok.order(8)
+    link = 'history'
+    text = _(u'History')
+
+
+class StudentsContainerManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudentsContainer)
+    grok.view(StudentsContainerPage)
+    grok.require('waeup.manageStudent')
+    text = _('Manage student 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):
+        if clearance_disabled_message(self.context):
+            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):
+        if clearance_disabled_message(self.context):
+            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 StudyCourseTranscriptActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IStudentStudyCourse)
+    grok.view(StudyCourseDisplayFormPage)
+    grok.require('waeup.viewTranscript')
+    text = _('Transcript')
+    target = 'transcript'
+    icon = 'actionicon_transcript.png'
+
+    @property
+    def target_url(self):
+        if self.context.student.transcript_enabled:
+            return self.view.url(self.view.context, self.target)
+        return False
+
+class TranscriptSlipActionButton(ManageActionButton):
+    grok.order(1)
+    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):
+        if self.context.student.transcript_enabled:
+            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):
+        is_current = self.context.__parent__.is_current
+        if self.context.student.state != REGISTERED or \
+            str(self.context.__parent__.current_level) != self.context.__name__ or\
+            not is_current:
+            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):
+        is_current = self.context.__parent__.is_current
+        if self.context.student.state not in (VALIDATED, REGISTERED) or \
+            str(self.context.__parent__.current_level) != self.context.__name__ or\
+            not is_current:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class CourseRegistrationSlipActionButton(ManageActionButton):
+    grok.order(5)
+    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 OnlinePaymentManageActionButton(ManageActionButton):
+#    grok.order(1)
+#    grok.context(IStudentPaymentsContainer)
+#    grok.view(PaymentsDisplayFormPage)
+#    grok.require('waeup.manageStudent')
+#    text = 'Manage payments'
+#    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 == 'paid':
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class AddBedTicketActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudentAccommodation)
+    grok.view(AccommodationManageFormPage)
+    grok.require('waeup.handleAccommodation')
+    icon = 'actionicon_home.png'
+    text = _('Book accommodation')
+    target = 'add'
+
+class BedTicketSlipActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IBedTicket)
+    grok.view(BedTicketDisplayFormPage)
+    grok.require('waeup.handleAccommodation')
+    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):
+        PWCHANGE_STATES = getUtility(IStudentsUtils).PWCHANGE_STATES
+        if self.context.state not in PWCHANGE_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 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 ProcessTranscriptRequestActionButton(ManageActionButton):
+    grok.order(9)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.viewTranscript')
+    text = _('Manage transcript request')
+    target = 'process_transcript_request'
+    icon = 'actionicon_transcript.png'
+
+    @property
+    def target_url(self):
+        if self.context.state != TRANSCRIPT:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class StudentsTab(PrimaryNavTab):
+    """Students tab in primary navigation.
+    """
+
+    grok.context(IKofaObject)
+    grok.order(4)
+    grok.require('waeup.viewStudentsTab')
+    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 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
+
+def handle_file_delete(context, view, download_name):
+    """Handle deletion of student 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 student 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.'))
+        return False
+    upload.seek(0) # file pointer moved when determining size
+    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.'))
+        return False
+    basename, expected_ext = os.path.splitext(download_name)
+    if expected_ext:
+        if '.' + file_format != expected_ext:
+            view.flash(_('${a} file extension expected.',
+                mapping = {'a':expected_ext[1:]}))
+            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)}))
+            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.context(IStudent)
+    grok.viewletmanager(FileManager)
+    grok.view(StudentClearanceDisplayFormPage)
+    template = default_filedisplay_template
+    grok.order(1)
+    grok.require('waeup.viewStudent')
+    label = _(u'File')
+    title = _(u'Scan')
+    download_name = u'filename.jpg'
+
+    @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.context(IStudent)
+    grok.viewletmanager(FileManager)
+    grok.view(StudentClearanceManageFormPage)
+    template = default_fileupload_template
+    grok.require('waeup.uploadStudentFile')
+    tab_redirect = '?tab2'
+    mus = 1024 * 150
+    upload_button =_('Upload new file')
+    delete_button = _('Delete attachment')
+
+    @property
+    def show_viewlet(self):
+        students_utils = getUtility(IStudentsUtils)
+        if self.__name__ in students_utils.SKIP_UPLOAD_VIEWLETS:
+            return False
+        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.'))
+                self.view.redirect(
+                    self.view.url(
+                        self.context, self.view.__name__) + self.tab_redirect)
+        return
+
+class PassportDisplay(FileDisplay):
+    """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(FileUpload):
+    """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)')
+    mus = 1024 * 50
+    download_name = u'passport.jpg'
+    tab_redirect = '?tab2'
+
+class PassportUploadEdit(PassportUploadManage):
+    """Passport upload viewlet for students.
+    """
+    grok.view(StudentFilesUploadPage)
+    grok.require('waeup.uploadStudentFile')
+
+class BirthCertificateDisplay(FileDisplay):
+    """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(ExportPDFClearanceSlipPage)
+
+class BirthCertificateUpload(FileUpload):
+    """Birth Certificate upload viewlet.
+    """
+    grok.order(1)
+    label = _(u'Birth Certificate')
+    title = _(u'Birth Certificate Scan')
+    mus = 1024 * 150
+    download_name = u'birth_certificate'
+    tab_redirect = '?tab2'
+
+class Image(grok.View):
+    """Renders images for students.
+    """
+    grok.baseclass()
+    grok.name('none.jpg')
+    grok.context(IStudent)
+    grok.require('waeup.viewStudent')
+    download_name = u'none.jpg'
+
+    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 == '.png':
+            self.response.setHeader('Content-Type', 'image/png')
+        elif ext == '.pdf':
+            self.response.setHeader('Content-Type', 'application/pdf')
+        elif ext == '.tif':
+            self.response.setHeader('Content-Type', 'image/tiff')
+        return image
+
+class Passport(Image):
+    """Renders jpeg passport picture.
+    """
+    grok.name('passport.jpg')
+    download_name = u'passport.jpg'
+    grok.context(IStudent)
+
+class ApplicationSlipImage(Image):
+    """Renders application slip scan.
+    """
+    grok.name('application_slip')
+    download_name = u'application_slip'
+
+class BirthCertificateImage(Image):
+    """Renders birth certificate scan.
+    """
+    grok.name('birth_certificate')
+    download_name = u'birth_certificate'
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/vocabularies.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/vocabularies.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/vocabularies.py	(revision 10886)
@@ -0,0 +1,215 @@
+## $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 student 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
+
+
+#: 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):
+    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 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.
+    """
+    def getValues(self, context):
+        return study_levels(context)
+
+    def getToken(self, context, value):
+        return str(value)
+
+    def getTitle(self, context, value):
+        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 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/tags/0.2/src/waeup/kofa/students/webservices.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/webservices.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/webservices.py	(revision 10886)
@@ -0,0 +1,229 @@
+## $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 xmlrpclib
+from zope.component import getUtility, queryUtility
+from zope.catalog.interfaces import ICatalog
+from waeup.kofa.interfaces import IUniversity, IExtFileStore
+
+
+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,
+                    )
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/students/workflow.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/students/workflow.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/students/workflow.py	(revision 10886)
@@ -0,0 +1,289 @@
+"""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, TRANSCRIPT)
+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)
+
+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 = _('Reset to school fee paid'),
+        msg = _("Reset to 'school fee paid'"),
+        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 = TRANSCRIPT),
+
+    Transition(
+        transition_id = 'process_transcript',
+        title = _('Transcript processed'),
+        msg = _("Transcript processed"),
+        source = TRANSCRIPT,
+        destination = GRADUATED),
+    )
+
+
+IMPORTABLE_TRANSITIONS = [i.transition_id for i in REGISTRATION_TRANSITIONS]
+
+FORBIDDEN_POSTGRAD_TRANS = ['reset6', 'register_courses']
+LOCK_CLEARANCE_TRANS = ('reset2', 'request_clearance')
+UNLOCK_CLEARANCE_TRANS = ('reset3', 'reset4', 'start_clearance')
+
+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)
+    if event.transition.transition_id in LOCK_CLEARANCE_TRANS:
+        obj.clearance_locked = True
+    if event.transition.transition_id in UNLOCK_CLEARANCE_TRANS:
+        obj.clearance_locked = False
+    # 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
+    # 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/tags/0.2/src/waeup/kofa/testing.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/testing.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/testing.py	(revision 10886)
@@ -0,0 +1,470 @@
+## $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 zc.async.interfaces import COMPLETED
+from zope.app.testing.functional import (
+    ZCMLLayer, FunctionalTestSetup, getRootFolder, sync, FunctionalTestCase)
+from zope.component import getGlobalSiteManager, queryUtility
+from zope.security.testing import addCheckerPublic
+from zope.testing import renormalizing
+from zope.testing.cleanup import cleanUp
+
+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):
+    old_loggers = get_all_loggers()
+    FunctionalTestSetup().setUp()
+
+def tearDown(test):
+    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, getGlobalSiteManager
+    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
+
+    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):
+        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
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/__init__.py	(revision 10886)
@@ -0,0 +1,2 @@
+# This is a package
+
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_app.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_app.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_app.py	(revision 10886)
@@ -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 test_update_plugins(self):
+        # We can update plugins
+        setSite(self.app)
+        del self.app['accesscodes']
+        self.app.updatePlugins()
+        self.assertTrue('accesscodes' 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/tags/0.2/src/waeup/kofa/tests/test_async.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_async.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_async.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/tests/test_authentication.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_authentication.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_authentication.py	(revision 10886)
@@ -0,0 +1,330 @@
+## $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
+from zope.component.hooks import setSite, clearSite
+from zope.interface.verify import verifyClass, verifyObject
+from zope.password.testing import setUpPasswordManagers
+from zope.pluggableauth.interfaces import IAuthenticatorPlugin
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from waeup.kofa.testing import FunctionalTestCase, FunctionalLayer
+from waeup.kofa.authentication import (
+    UserAuthenticatorPlugin, Account, KofaPrincipalInfo, FailedLoginInfo,
+    get_principal_role_manager, UsersPlugin,)
+from waeup.kofa.interfaces import (
+    IUserAccount, IFailedLoginInfo, IKofaPrincipalInfo, IKofaPluggable)
+
+class FakeSite(grok.Site, grok.Container):
+    #def getSiteManager(self):
+    #    return None
+    #    return getGlobalSiteManager()
+    pass
+
+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
+
+class UsersPluginTests(unittest.TestCase):
+
+    def setUp(self):
+        setUpPasswordManagers()
+        self.site = FakeSite()
+        self.site['users'] = grok.Container()
+        return
+
+    def get_logger(self):
+        logger = logging.getLogger('waeup.test')
+        stream = StringIO()
+        handler = logging.StreamHandler(stream)
+        logger.setLevel(logging.DEBUG)
+        logger.propagate = False
+        logger.addHandler(handler)
+        return logger, stream
+
+    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 = self.get_logger()
+        plugin.update(self.site, 'app', logger)
+        stream.seek(0)
+        self.assertEqual(stream.read(), '')
+        self.site['users']['bob'] = FakeUserAccount()
+        logger, stream = self.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
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_beaker.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_beaker.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_beaker.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/tests/test_catalog.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_catalog.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_catalog.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/tests/test_configuration.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_configuration.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_configuration.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/tests/test_datacenter.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_datacenter.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_datacenter.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/tests/test_doctests.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_doctests.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_doctests.py	(revision 10886)
@@ -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([
+            'authentication.txt',
+            ])
+    return suite
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_fees.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_fees.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_fees.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/tests/test_imagestorage.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_imagestorage.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_imagestorage.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/tests/test_interfaces.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_interfaces.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_interfaces.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/tests/test_maintenance.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_maintenance.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_maintenance.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/tests/test_objecthistory.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_objecthistory.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_objecthistory.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/tests/test_reports.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_reports.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_reports.py	(revision 10886)
@@ -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)
+        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/tags/0.2/src/waeup/kofa/tests/test_schoolgrades.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_schoolgrades.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_schoolgrades.py	(revision 10886)
@@ -0,0 +1,86 @@
+# 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
+
+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/tags/0.2/src/waeup/kofa/tests/test_smtp.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_smtp.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_smtp.py	(revision 10886)
@@ -0,0 +1,377 @@
+# -*- 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[:-2]
+        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 sender@example.com to 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 <sender@example.com>',
+             u'msg: Cc: A sender <sender@example.com>',
+             u'msg: To: A recipient <recpt@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 sender@example.com 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 <sender@example.com>',
+             u'msg: Cc: A sender <sender@example.com>',
+             u'msg: To: A recipient <recpt@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 sender@example.com 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 <sender@example.com>',
+             u'msg: Cc: A sender <sender@example.com>',
+             u'msg: To: A recipient <recpt@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/tags/0.2/src/waeup/kofa/tests/test_sourcefactory.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_sourcefactory.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_sourcefactory.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/tests/test_startup.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_startup.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_startup.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/tests/test_waeup.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_waeup.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/test_waeup.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/tests/util.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/util.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/tests/util.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/university/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/__init__.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/university/api.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/api.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/api.txt	(revision 10886)
@@ -0,0 +1,30 @@
+:mod:`waeup.kofa.university` -- academic components
+***************************************************
+
+.. module:: waeup.kofa.university
+
+Components that represent academic 'core' stuff like faculties, 
+
+Submodules
+==========
+
+The :mod:`waeup.kofa.university` module provides the following submodules:
+
+.. toctree::
+   :maxdepth: 1
+
+   certificate.txt
+   certificatescontainer.txt
+   course.txt
+   coursescontainer.txt
+   department.txt
+   faculty.txt
+   facultiescontainer.txt
+
+Additional infos about the referrers of courses and how they are
+handled can be found in:
+
+.. toctree::
+   :maxdepth: 1
+
+   certcourses.txt
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/university/batching.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/batching.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/batching.py	(revision 10886)
@@ -0,0 +1,398 @@
+## $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
+
+class FacultyProcessor(BatchProcessor):
+    """A batch processor for IFaculty objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'facultyprocessor'
+    grok.name(util_name)
+
+    name = u'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):
+    """A batch processor for IDepartment objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'departmentprocessor'
+    grok.name(util_name)
+
+    name = u'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):
+    """A batch processor for ICertificate objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'certificateprocessor'
+    grok.name(util_name)
+
+    name = u'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 \
+                and 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. Makes only sense in update mode but
+        # does also work in create mode.
+        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):
+    """A batch processor for ICourse objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'courseprocessor'
+    grok.name(util_name)
+
+    name = u'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):
+    """A batch processor for ICertificateCourse objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'certificatecourseprocessor'
+    grok.name(util_name)
+
+    name = u'CertificateCourse Processor'
+    iface = ICertificateCourse
+
+    location_fields = ['course', 'level', 'faculty_code', 'department_code',
+                       'certificate_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)
+        parent.addCertCourse(row['course'],
+                            row['level'], row['mandatory'])
+        return
+
+    def delEntry(self, row, site):
+        parent = self.getParent(row, site)
+        parent.delCertCourses(row['course'].code, row['level'])
+        return
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/university/catalog.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/catalog.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/catalog.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/university/certcourses.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/certcourses.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/certcourses.txt	(revision 10886)
@@ -0,0 +1,141 @@
+Certificate Courses
+*******************
+
+Courses are referred to by certificate courses.
+
+We can 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, certificats
+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/tags/0.2/src/waeup/kofa/university/certificate.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/certificate.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/certificate.py	(revision 10886)
@@ -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
+##
+"""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',
+        ]
+
+    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,
+                 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.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):
+        """Add a certificate course.
+        """
+        code = "%s_%s" % (course.code, level)
+        self[code] = CertificateCourse(course, level, mandatory)
+        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, fac, dep):
+        self.moved = True
+        cert = self
+        del self.__parent__[cert.code]
+        grok.getSite()['faculties'][fac][dep].certificates[cert.code] = cert
+        self.__parent__._p_changed = True
+        cat = getUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(certcode=(cert.code, cert.code))
+        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):
+        self.course = course
+        self.level = level
+        self.mandatory = mandatory
+
+    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
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/university/certificate.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/certificate.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/certificate.txt	(revision 10886)
@@ -0,0 +1,366 @@
+:mod:`waeup.kofa.university.certificate` -- Certificates for Kofa
+******************************************************************
+
+.. 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>
+
+
+Event Subscribers
+=================
+
+.. function:: removedCourseHandler(course, event)
+
+   An event subscriber triggered for
+   :class:`grok.IObjectRemovedEvent`s, when an :class:`ICourse`
+   instance is removed from a container.
+
+   Tries to remove all referring :class:`CertificateCourse`
+   instances that refer to a removed course.
+
+   To accomplish that, the parents of the removed course are looked up
+   for a certifcate container which contains a certificate course that
+   refers to the deleted course.
+
+   .. seealso:: :ref:`removecertificatecourses`
+
+   **handles:**
+     :class:`ICourse`
+
+   **event type:**
+     :class:`grok.IObjectRemovedEvent`
+
+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...>
+
+CertificateCourses
+------------------
+
+: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...>
+
+.. _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.
+
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/university/certificatescontainer.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/certificatescontainer.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/certificatescontainer.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/university/certificatescontainer.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/certificatescontainer.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/certificatescontainer.txt	(revision 10886)
@@ -0,0 +1,147 @@
+:mod:`waeup.kofa.university.certificatescontainer` -- 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/tags/0.2/src/waeup/kofa/university/course.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/course.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/course.py	(revision 10886)
@@ -0,0 +1,155 @@
+## $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
+
+    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/tags/0.2/src/waeup/kofa/university/course.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/course.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/course.txt	(revision 10886)
@@ -0,0 +1,143 @@
+:mod:`waeup.kofa.university.course` -- 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/tags/0.2/src/waeup/kofa/university/coursescontainer.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/coursescontainer.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/coursescontainer.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/university/coursescontainer.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/coursescontainer.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/coursescontainer.txt	(revision 10886)
@@ -0,0 +1,145 @@
+:mod:`waeup.kofa.university.coursescontainer` -- 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/tags/0.2/src/waeup/kofa/university/department.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/department.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/department.py	(revision 10886)
@@ -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
+##
+"""University departments.
+"""
+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.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.utils.helpers import attrs_to_fields
+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.CourseAdviser100',
+        'waeup.local.CourseAdviser200',
+        'waeup.local.CourseAdviser300',
+        'waeup.local.CourseAdviser400',
+        'waeup.local.CourseAdviser500',
+        'waeup.local.CourseAdviser600',
+        'waeup.local.CourseAdviser700',
+        'waeup.local.CourseAdviser800',
+        ]
+
+    def __init__(self,
+                 title=u'Unnamed Department',
+                 title_prefix=u'department',
+                 code=u"NA", **kw):
+        super(Department, self).__init__(**kw)
+        self.title = title
+        self.title_prefix = title_prefix
+        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
+        or departments.
+        """
+        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)
+
+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 courses.
+    """
+
+    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/tags/0.2/src/waeup/kofa/university/department.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/department.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/department.txt	(revision 10886)
@@ -0,0 +1,135 @@
+:mod:`waeup.kofa.university.department` -- 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/tags/0.2/src/waeup/kofa/university/export.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/export.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/export.py	(revision 10886)
@@ -0,0 +1,226 @@
+## $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):
+    """Exporter for faculties.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('faculties')
+
+    #: Fieldnames considered by this exporter
+    fields = ('code', 'title', 'title_prefix', 'users_with_local_roles')
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Faculties')
+
+    def mangle_value(self, value, name, context=None):
+        """Hook for mangling values in derived classes
+        """
+        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):
+    """Exporter for departments.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('departments')
+
+    #: Fieldnames considered by this exporter
+    fields = ('code', 'faculty_code', 'title', 'title_prefix',
+        'users_with_local_roles')
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Departments')
+
+    def mangle_value(self, value, name, context=None):
+        """Hook for mangling values in derived classes
+        """
+        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):
+    """Exporter for courses.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('courses')
+
+    #: Fieldnames considered by this exporter
+    fields = ('code', 'faculty_code', 'department_code', 'title', 'credits',
+              'passmark', 'semester', 'users_with_local_roles', 'former_course')
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Courses')
+
+    def mangle_value(self, value, name, context=None):
+        """Hook for mangling values in derived classes
+        """
+        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):
+    """Exporter for courses.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('certificates')
+
+    #: Fieldnames considered by this exporter
+    fields = ('code', 'faculty_code', 'department_code', 'title', 'study_mode',
+              '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')
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Certificates')
+
+    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):
+    """Exporter for courses.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('certificate_courses')
+
+    #: Fieldnames considered by this exporter
+    fields = ('course', 'faculty_code', 'department_code', 'certificate_code',
+              'level', 'mandatory')
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Courses in Certificates')
+
+    def mangle_value(self, value, name, context=None):
+        """Hook for mangling values in derived classes
+        """
+        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/tags/0.2/src/waeup/kofa/university/facultiescontainer.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/facultiescontainer.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/facultiescontainer.py	(revision 10886)
@@ -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('faculties')
+
+    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/tags/0.2/src/waeup/kofa/university/facultiescontainer.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/facultiescontainer.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/facultiescontainer.txt	(revision 10886)
@@ -0,0 +1,178 @@
+:mod:`waeup.kofa.university.facultiescontainer` -- 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/tags/0.2/src/waeup/kofa/university/faculty.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/faculty.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/faculty.py	(revision 10886)
@@ -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
+##
+"""Faculty components.
+"""
+
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.component import getUtility
+from waeup.kofa.interfaces import IKofaUtils
+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 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',
+        ]
+
+    def __init__(self,
+                 title=u'Unnamed Faculty',
+                 title_prefix=u'faculty',
+                 code=u'NA', **kw):
+        super(Faculty, self).__init__(**kw)
+        self.title = title
+        self.title_prefix = title_prefix
+        self.code = code
+
+    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)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/university/faculty.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/faculty.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/faculty.txt	(revision 10886)
@@ -0,0 +1,200 @@
+:mod:`waeup.kofa.university.faculty` -- 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/tags/0.2/src/waeup/kofa/university/interfaces.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/interfaces.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/interfaces.py	(revision 10886)
@@ -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
+##
+"""Interfaces of academics specific objects.
+"""
+
+from zope import schema
+from zope.interface import Attribute, invariant, Invalid
+from waeup.kofa.interfaces import (IKofaObject, IKofaContainer)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.university.vocabularies import (
+    course_levels,
+    CourseSource,
+    StudyModeSource,
+    AppCatSource,
+    InstTypeSource,
+    SemesterSource,
+    )
+
+class IFaculty(IKofaContainer):
+    """Representation of a university faculty.
+    """
+    code = schema.TextLine(
+        title = _(u'Code'),
+        default = u'NA',
+        required = True,
+        )
+
+    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,
+        )
+
+
+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,
+        )
+
+    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,
+        )
+
+    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,
+        )
+
+    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,
+        )
+
+
+class ICertificate(IKofaObject):
+    """Representation of a certificate.
+    """
+    code = schema.TextLine(
+        title = _(u'Code'),
+        default = u'NA',
+        required = True,
+        )
+
+    title = schema.TextLine(
+        title = _(u'Title'),
+        default = u'Unnamed Certificate',
+        required = True,
+        )
+
+    study_mode = schema.Choice(
+        title = _(u'Study Mode'),
+        source = StudyModeSource(),
+        default = u'ug_ft',
+        required = True,
+        )
+
+    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,
+        )
+
+    school_fee_2 = schema.Float(
+        title = _(u'Returning School Fee'),
+        required = False,
+        )
+
+    school_fee_3 = schema.Float(
+        title = _(u'Foreigner Initial School Fee'),
+        required = False,
+        )
+
+    school_fee_4 = schema.Float(
+        title = _(u'Foreigner Returning School Fee'),
+        required = False,
+        )
+
+    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,
+        )
+
+    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/tags/0.2/src/waeup/kofa/university/tests/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/__init__.py	(revision 10886)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/sample_certificate_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/sample_certificate_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/sample_certificate_data.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/university/tests/sample_course_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/sample_course_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/sample_course_data.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/university/tests/sample_course_data_update.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/sample_course_data_update.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/sample_course_data_update.csv	(revision 10886)
@@ -0,0 +1,2 @@
+code,title
+CRS2,"New Title"
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/sample_department_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/sample_department_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/sample_department_data.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/university/tests/sample_faculty_data.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/sample_faculty_data.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/sample_faculty_data.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/university/tests/sample_faculty_data_update.csv
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/sample_faculty_data_update.csv	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/sample_faculty_data_update.csv	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/university/tests/test_batching.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_batching.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_batching.py	(revision 10886)
@@ -0,0 +1,713 @@
+## $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(',')
+
+class UniversityProcessorSetup(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(UniversityProcessorSetup, self).setUp()
+        self.dc_root = tempfile.mkdtemp()
+        self.workdir = tempfile.mkdtemp()
+        app = University()
+        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 - Department Processor - sample_department_data '
+            '- DEP2 - updated: local_roles=anne|waeup.local.DepartmentManager, '
+            'code=DEP2\n'
+            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.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)
+        # 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. Skipping\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. Skipping\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. Skipping\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(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CertCourseProcessorTests, self).setUp()
+        self.proc = CertificateCourseProcessor()
+        self.site0 = dict()
+        self.site1 = dict(faculties=dict())
+        self.site2 = dict(faculties=dict(FAC=dict()))
+        self.department1 = Department(code='DPT')
+        self.department2 = Department(code='DPT')
+        self.certificate = Certificate(code='CRT')
+        self.department2.certificates['CRT'] = self.certificate
+        self.course = Course(code='CRS')
+        self.department2.certificates['CRT'].addCertCourse(self.course)
+        self.cert_course = self.certificate['CRS_100']
+        self.department2.courses['CRS'] = self.course
+        self.site3 = dict(faculties=dict(FAC=dict(DPT=self.department1)))
+        self.site4 = dict(faculties=dict(FAC=dict(DPT=self.department2)))
+        self.row = dict(
+            department_code='DPT',
+            faculty_code='FAC',
+            certificate_code='CRT',
+            course=self.course, level='100',
+            code='CRS_100')
+        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
+        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)
+        result4 = self.proc.parentsExist(self.row, self.site4)
+        self.assertTrue(result0 is False)
+        self.assertTrue(result1 is False)
+        self.assertTrue(result2 is False)
+        self.assertTrue(result3 is False)
+        self.assertTrue(result4 is True)
+        return
+
+    def test_entry_exists(self):
+        # make sure we find an entry if it exists
+        result0 = self.proc.entryExists(self.row, self.site0)
+        result1 = self.proc.entryExists(self.row, self.site1)
+        result2 = self.proc.entryExists(self.row, self.site2)
+        result3 = self.proc.entryExists(self.row, self.site3)
+        result4 = self.proc.entryExists(self.row, self.site4)
+        self.assertTrue(result0 is False)
+        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.row, self.site3)
+        result2 = self.proc.getEntry(self.row, self.site4)
+        self.assertTrue(result1 is None)
+        self.assertTrue(result2 is self.cert_course)
+        return
+
+    def test_del_entry(self):
+        # make sure we can delete entries
+        self.assertTrue('CRS_100' in self.certificate.keys())
+        self.proc.delEntry(self.row, self.site4)
+        self.assertTrue('CRS_100' not in self.certificate.keys())
+        return
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_certificatescontainer.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_certificatescontainer.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_certificatescontainer.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/university/tests/test_coursescontainer.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_coursescontainer.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_coursescontainer.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/university/tests/test_export.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_export.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_export.py	(revision 10886)
@@ -0,0 +1,558 @@
+## $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\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\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\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\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')
+        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\r\n'
+            'D1,F1,Department of Cheddar,department,'
+            '"[{\'user_name\': u\'bob\', \'local_role\': u\'bobsrole\'}]"\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\r\n'
+            'D1,F1,Department of Cheddar,department,'
+            '"[{\'user_name\': u\'bob\', \'local_role\': u\'bobsrole\'}]"\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\r\n'
+            'D1,F1,Department of Cheddar,department,'
+            '"[{\'user_name\': u\'bob\', \'local_role\': u\'bobsrole\'}]"\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\r\n'
+            'D1,F1,Department of Cheddar,department,'
+            '"[{\'user_name\': u\'bob\', \'local_role\': u\'bobsrole\'}]"\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\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\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\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\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,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,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,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,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\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\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\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\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/tags/0.2/src/waeup/kofa/university/tests/test_facultiescontainer.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_facultiescontainer.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_facultiescontainer.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/university/tests/test_faculty.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_faculty.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_faculty.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/university/tests/test_university.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_university.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/tests/test_university.py	(revision 10886)
@@ -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(['university/certcourses.txt',])
+    # add local unittests (actually only one)
+    for testcase in [UniversitySubpackageTests,]:
+        suite.addTests(unittest.TestLoader().loadTestsFromTestCase(
+                testcase))
+    return suite
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/university/vocabularies.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/university/vocabularies.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/university/vocabularies.py	(revision 10886)
@@ -0,0 +1,129 @@
+## $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 academics 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
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.sourcefactory import SmartBasicContextualSourceFactory
+from waeup.kofa.utils.utils import KofaUtils
+
+course_levels = SimpleKofaVocabulary(
+    (_('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),
+    )
+
+#: An instance of :class:`waeup.kofa.utils.utils.KofaUtils` for fallback.
+KOFA_UTILS = KofaUtils()
+
+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 = queryUtility(IKofaUtils, default=KOFA_UTILS)
+        return sorted(getattr(utils, self.DICT_NAME).keys())
+
+    def getToken(self, context, value):
+        return str(value)
+
+    def getTitle(self, context, value):
+        utils = queryUtility(IKofaUtils, default=KOFA_UTILS)
+        return getattr(utils, self.DICT_NAME)[value]
+
+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 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 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/tags/0.2/src/waeup/kofa/userscontainer.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/userscontainer.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/userscontainer.py	(revision 10886)
@@ -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
+##
+"""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 principals.
+
+    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
+        #if description is None:
+        #    description = title
+        self[name] = Account(name=name, password=password, title=title,
+                             description=description, public_name=public_name,
+                             email=email, phone=phone, roles=roles)
+        #self.logger.info('User account %s added.' % name)
+
+    def addAccount(self, account):
+        """Add the account passed.
+        """
+        self[account.name] = account
+        #self.logger.info('User account %s added.' % account.name)
+
+    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]
+            #self.logger.info('User account %s deleted.' % name)
+
+
+class UsersExporter(grok.GlobalUtility, ExporterBase):
+    """Exporter for user accounts.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('users')
+
+    #: Fieldnames considered by this exporter
+    fields = ('name', 'title', 'public_name', 'description',
+              'email', 'phone', 'roles', 'local_roles', 'password')
+
+    #: The title under which this exporter will be displayed
+    title = _(u'Users')
+
+    def mangle_value(self, value, name, context=None):
+        """Treat location values special.
+        """
+
+        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(
+            UsersExporter, 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/tags/0.2/src/waeup/kofa/userscontainer.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/userscontainer.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/userscontainer.txt	(revision 10886)
@@ -0,0 +1,80 @@
+User container for the Kofa
+***************************
+
+.. :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 UsersExporter
+    >>> import os
+    >>> import tempfile
+    >>> workdir = tempfile.mkdtemp()
+    >>> outfile = os.path.join(workdir, 'myoutput.csv')
+    >>> exporter = UsersExporter()
+    >>> 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/tags/0.2/src/waeup/kofa/utils/api.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/api.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/api.txt	(revision 10886)
@@ -0,0 +1,19 @@
+:mod:`waeup.kofa.utils` -- misc. helpers
+****************************************
+
+.. module:: waeup.kofa.utils
+
+Components and other stuff of general interest.
+
+Submodules
+==========
+
+The :mod:`waeup.kofa.utils` module provides the following submodules:
+
+.. toctree::
+   :maxdepth: 1
+
+   helpers.txt
+   batching.txt
+   importexport.txt
+   converters.txt
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/batching.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/batching.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/batching.py	(revision 10886)
@@ -0,0 +1,767 @@
+## $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.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 = 'baseprocessor'
+
+    # 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 = ['code', 'faculty_code']
+
+    # A factory with this name must be registered...
+    factory_name = 'waeup.Department'
+
+    @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 parent 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 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 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
+            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 doImport(self, path, headerfields, mode='create', user='Unknown',
+                 logger=None, ignore_empty=True):
+        """Perform actual import.
+        """
+        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
+            string_row = self.applyMapping(raw_row, mapping)
+            if ignore_empty and mode in ('update',):
+                # replace empty strings with ignore-markers
+                for key, val in string_row.items():
+                    if 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. Skipping")
+                    continue
+                if self.entryExists(row, site):
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "This object already exists. Skipping.")
+                    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,
+                        "%s Skipping." % error.message)
+                    continue
+                except DuplicationError, error:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "%s Skipping." % error.msg)
+                    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
+                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
+                self.updateEntry(obj, row, site, base)
+            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)
+        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 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 `args` and `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_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)
+    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/tags/0.2/src/waeup/kofa/utils/batching.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/batching.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/batching.txt	(revision 10886)
@@ -0,0 +1,556 @@
+:mod:`waeup.kofa.utils.batching` -- 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. Skipping.
+    Wilma,Wilmas Asylum,1,1,This object already exists. Skipping.
+    Fred,Freds Dinoburgers,0,10,This object already exists. Skipping.
+    Joey,Joeys Drive-in,0,110,This object already exists. Skipping.
+
+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. Skipping.
+    Wilmas Asylum,1,This object already exists. Skipping.
+    Freds Dinoburgers,10,This object already exists. Skipping.
+    Joeys Drive-in,110,This object already exists. Skipping.
+
+
+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/tags/0.2/src/waeup/kofa/utils/browser.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/browser.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/browser.py	(revision 10886)
@@ -0,0 +1,200 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""UI components for utilities and helpers.
+"""
+
+import grok
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility, createObject
+from waeup.kofa.browser.layout import UtilityView
+from waeup.kofa.interfaces import IObjectHistory
+
+from waeup.kofa.interfaces import IUniversity
+
+def replaceStudentMessages(old, new):
+    students = grok.getSite()['students']
+    for student in students.values():
+        history = IObjectHistory(student)
+        history.modifyMessages(old, new)
+    return
+
+def replaceApplicantMessages(old, new):
+    applicants = grok.getSite()['applicants']
+    for container in applicants.values():
+        for applicant in container.values():
+            history = IObjectHistory(applicant)
+            history.modifyMessages(old, new)
+    return
+
+def removeStudentMessage(student_id, number):
+    students = grok.getSite()['students']
+    student = students.get(student_id, None)
+    if student:
+        history = IObjectHistory(student)
+        success, text = history.removeMessage(number)
+    return success, text
+
+def removeApplicantMessage(applicant_id, number):
+    applicants = grok.getSite()['applicants']
+    try:
+        container, application_number = applicant_id.split('_')
+    except:
+        return False, 'applicant_id is wrong'
+    container = applicants.get(container, None)
+    if not container:
+        return False, 'No such container'
+    applicant = container.get(application_number, None)
+    if applicant is None:
+        return False, 'No such applicant'
+    history = IObjectHistory(applicant)
+    success, text = history.removeMessage(number)
+    return success, text
+
+class ReindexPage(UtilityView, grok.View):
+    """ Reindex view.
+
+    Reindexes a catalog. For managers only.
+    """
+    grok.context(IUniversity)
+    grok.name('reindex')
+    grok.require('waeup.managePortal')
+
+    def update(self,ctlg=None):
+        if ctlg is None:
+            self.flash('No catalog name provided.')
+            return
+        cat = queryUtility(ICatalog, name='%s_catalog' % ctlg)
+        if cat is None:
+            self.flash('%s_catalog does not exist' % ctlg)
+            return
+        self.context.logger.info(
+            'Catalog `%s_catalog` re-indexing started.' % ctlg)
+        cat.updateIndexes()
+        no_of_entries = cat.values()[0].documentCount()
+        self.flash('%d %s re-indexed.' % (no_of_entries,ctlg))
+        self.context.logger.info(
+            'Re-indexing of %d objects finished.' % no_of_entries)
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+class ModifyAllStudentHistory(UtilityView, grok.View):
+    """ View to modify all student histories.
+
+    """
+    grok.context(IUniversity)
+    grok.name('modify_student_history')
+    grok.require('waeup.managePortal')
+
+    def update(self,old=None, new=None):
+        if None in (old, new):
+            self.flash('Syntax: /modify_student_history?old=[old string]&new=[new string]')
+            return
+        replaceStudentMessages(old, new)
+        self.context.logger.info(
+            "'%s' replaced by '%s' in all student histories." % (old, new))
+        self.flash('Finished')
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+class RemoveStudentHistoryMessage(UtilityView, grok.View):
+    """ View to remove a single student history entry.
+
+    """
+    grok.context(IUniversity)
+    grok.name('remove_student_history_message')
+    grok.require('waeup.managePortal')
+
+    def update(self,student_id=None, number=None):
+        if None in (student_id, number):
+            self.flash('Syntax: /remove_student_history_message?student_id=[id]&number=[line number, starting with 0]')
+            return
+        try:
+            number=int(number)
+        except:
+            self.flash('Error')
+            return
+        success, text = removeStudentMessage(student_id, number)
+        if not success:
+            self.flash('Error: %s' % text)
+            return
+        self.context.logger.info(
+            "line '%s' removed in %s history" % (text, student_id))
+        self.flash('Finished')
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+class ModifyAllApplicantHistory(UtilityView, grok.View):
+    """ View to modify all student histories.
+
+    """
+    grok.context(IUniversity)
+    grok.name('modify_applicant_history')
+    grok.require('waeup.managePortal')
+
+    def update(self,old=None, new=None):
+        if None in (old, new):
+            self.flash('Syntax: /modify_applicant_history?old=[old string]&new=[new string]')
+            return
+        replaceApplicantMessages(old, new)
+        self.context.logger.info(
+            "'%s' replaced by '%s' in all applicant histories." % (old, new))
+        self.flash('Finished')
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
+
+class RemoveApplicantHistoryMessage(UtilityView, grok.View):
+    """ View to remove a single applicant history entry.
+
+    """
+    grok.context(IUniversity)
+    grok.name('remove_applicant_history_message')
+    grok.require('waeup.managePortal')
+
+    def update(self,applicant_id=None, number=None):
+        if None in (applicant_id, number):
+            self.flash('Syntax: /remove_applicant_history_message?applicant_id=[id]&number=[line number, starting with 0]')
+            return
+        try:
+            number=int(number)
+        except:
+            self.flash('Error')
+            return
+        success, text = removeApplicantMessage(applicant_id, number)
+        if not success:
+            self.flash('Error: %s' % text)
+            return
+        self.context.logger.info(
+            "line '%s' removed in %s history" % (text, applicant_id))
+        self.flash('Finished')
+        return
+
+    def render(self):
+        self.redirect(self.url(self.context, '@@index'))
+        return
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/converters.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/converters.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/converters.py	(revision 10886)
@@ -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
+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:
+                # convert field ValidationError to WidgetInputError
+                error = WidgetInputError(widget.name, widget.label, error)
+                errors.append((name, error))
+            except InputErrors, 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/tags/0.2/src/waeup/kofa/utils/converters.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/converters.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/converters.txt	(revision 10886)
@@ -0,0 +1,861 @@
+:mod:`waeup.kofa.utils.converters` -- Converters
+************************************************
+
+.. module:: waeup.kofa.utils.converters
+
+Converters for :mod:`zope.schema` based data.
+
+.. contents::
+
+
+.. :NOdoctest:
+.. :NOlayer: waeup.kofa.testing.KofaUnitTestLayer
+
+
+Constants
+=========
+
+.. attribute:: NONE_STRING_VALUE
+
+   This value determines, which string represents
+   'non-values'. Current setting is:
+
+      >>> from waeup.kofa.utils.converters import NONE_STRING_VALUE
+      >>> NONE_STRING_VALUE
+      ''
+
+   If this value is found during conversion from strings, the value to
+   set will become ``None``.
+
+
+Adapters
+========
+
+The :mod:`waeup.kofa.utils.converters` module is basically a collection of
+adapters for field types of :mod:`zope.schema`.
+
+:class:`Converter`
+------------------
+
+.. class:: Converter(field)
+
+   Base class for converter classes. You cannot create working
+   instances of this class. Instead derive from it and implement the
+   missing classes.
+
+   `field` should be an object implementing some interface from
+   :mod:`zope.schema` like :class:`zope.schema.TextLine`,
+   `zope.schema.Choice` or similar.
+
+   .. method:: provides(waeup.kofa.interfaces.ISchemaTypeConverter)
+
+   .. method:: _convertValueFromString(string)
+
+       Raises :exc:`NotImplementedError`.
+
+       Called by :meth:`fromString()` to convert a given string to a
+       value appropriate for the applied field type. The string might
+       be ``None`` or ``missing_value`` from field definition.
+
+       Override this method in derived classes to get a working
+       converter.
+
+   .. method:: _convertValueToString(value)
+
+       Raises :exc:`NotImplementedError`.
+
+       Called by :meth:`toString()` to convert a given value to a
+       string suitable to be stored for instance in CSV files.
+
+       Override this method in derived classes to get a working
+       converter.
+
+   .. method:: fromString(string[, strict=True])
+
+       Compute a value for the context field from the given
+       `string`. Be aware that this might fail for several reasons,
+       for instance because the string cannot be converted to an
+       integer (if the applied field is an Integer field), etc.
+
+       Use `strict` to enable/disable validations of transformed
+       data.
+
+   .. method:: toString(value[, strict=True])
+
+       Generate a string from ``value``, suitable to be stored in CSV
+       files or similar. This method does preliminary checks, handles
+       defaults and ``missing_value`` stuff and then calls
+       :meth:`_convertValueToString(value)` to do the actual
+       conversion.
+
+       Use `strict` to enable/disable validations of transformed
+       data.
+       
+
+
+:class:`TextConverter`
+----------------------
+
+.. class:: TextConverter(field)
+
+   Create a converter for text fields.
+
+   `field` must be an object implementing
+   ``zope.schema.interfaces.IText``. This is true for instance for
+   :class:`zope.schema.TextLine` fields and most other, purely
+   text-related field types.
+
+   Instances of this class are available as adapters from :mod:`IText`
+   to :mod:`ISchemaTypeConverter`.
+
+   .. method:: adapts(zope.schema.IText)
+
+   .. method:: provides(waeup.kofa.interfaces.ISchemaTypeConverter)
+
+   .. method:: fromString(string[, strict=True])
+
+       Compute a unicode string value from the given `string`. Be
+       aware that this might fail if no conversion of the delivered
+       data is possible.
+
+       Use `strict` to enable/disable validations of transformed
+       data.
+
+   .. method:: toString(value[, strict=True])
+
+       Return `value` as string or ``None``.
+
+       If `value` is ``None`` (or field's `missing_value`), ``None``
+       is returned.
+
+       If the value is not valid for the applied field an exception
+       might be raised.
+
+       Use `strict` to enable/disable validations of input data.
+
+
+:class:`BoolConverter`
+----------------------
+
+.. class:: BoolConverter(field)
+
+   Create a converter for boolean fields.
+
+   `field` must be an object implementing
+   ``zope.schema.interfaces.IBool``.
+
+   Instances of this class are available as adapters from :mod:`IBool`
+   to :mod:`ISchemaTypeConverter`.
+
+   .. method:: adapts(zope.schema.IBool)
+
+   .. method:: provides(waeup.kofa.interfaces.ISchemaTypeConverter)
+
+   .. method:: fromString(string[, strict=True])
+
+       Compute an integer number value from the given `string`. In
+       general '1', 'true', and 'yes' are interpreted as ``True``,
+       ``None`` is interpreted as 'no value' and all other values are
+       interpreted as ``True``.
+
+       Use `strict` to enable/disable validations of transformed
+       data.
+
+   .. method:: toString(value[, strict=True])
+
+       Return `value` as string or ``None``.
+
+       If `value` is ``None`` (or field's `missing_value`), ``None``
+       is returned. Otherwise ``0`` or ``1`` are returned.
+
+       If the value is not valid for the applied field (i.e. not an
+       integer or `missing_value`) an exception
+       might be raised.
+
+       Use `strict` to enable/disable validations of input data.
+
+
+:class:`IntConverter`
+---------------------
+
+.. class:: IntConverter(field)
+
+   Create a converter for integer fields.
+
+   `field` must be an object implementing
+   ``zope.schema.interfaces.IInt``.
+
+   Instances of this class are available as adapters from :mod:`IInt`
+   to :mod:`ISchemaTypeConverter`.
+
+   .. method:: adapts(zope.schema.IInt)
+
+   .. method:: provides(waeup.kofa.interfaces.ISchemaTypeConverter)
+
+   .. method:: fromString(string[, strict=True])
+
+       Compute an integer number value from the given `string`. Be
+       aware that this might fail if no conversion of the delivered
+       data is possible.
+
+       Use `strict` to enable/disable validations of transformed
+       data.
+
+   .. method:: toString(value[, strict=True])
+
+       Return `value` as string or ``None``.
+
+       If `value` is ``None`` (or field's `missing_value`), ``None``
+       is returned.
+
+       If the value is not valid for the applied field (i.e. not an
+       integer or `missing_value`) an exception
+       might be raised.
+
+       Use `strict` to enable/disable validations of input data.
+
+
+
+:class:`ChoiceConverter`
+------------------------
+
+.. class:: ChoiceConverter(field)
+
+   Create a converter for choice fields.
+
+   `field` must be an object implementing
+   ``zope.schema.interfaces.IChoice``.
+
+   Instances of this class are available as adapters from :mod:`IChoice`
+   to :mod:`ISchemaTypeConverter`.
+
+   .. method:: adapts(zope.schema.IChoice)
+
+   .. method:: provides(waeup.kofa.interfaces.ISchemaTypeConverter)
+
+   .. method:: fromString(string[, strict=False])
+
+       Compute a value from the given `string`. Be aware that this
+       might fail if no conversion of the delivered data is possible.
+
+       As choices generally require a certain set of allowed values,
+       only these values be allowed, in their tokenized form. The
+       converter takes the given string as token and then tries to
+       generate the appropriate value out of it (see examples below).
+
+       Use `strict` to enable/disable validations of transformed
+       data.
+
+       As an invalid value will result in a `KeyError`, additional
+       validation checks are disabled with this converter. This speeds
+       up things as expensive lookups are avoided.
+
+   .. method:: toString(value[, strict=False])
+
+       Return `value` as string or ``None``.
+
+       If `value` is ``None`` (or field's `missing_value`), ``None``
+       is returned.
+
+       If the value is not valid for the applied field (i.e. not an
+       integer or `missing_value`) an exception
+       might be raised.
+
+       Use `strict` to enable/disable validations of input data.
+
+       As an invalid value will result in a `ValueError`, additional
+       validation checks are disabled with this converter. This speeds
+       up things as expensive lookups are avoided.
+
+
+:class:`DateConverter`
+----------------------
+
+.. autoclass:: waeup.kofa.utils.converters.DateConverter
+   :members:
+   :inherited-members:
+
+
+Examples
+========
+
+Text field converters
+---------------------
+
+We create a field that also implements ``IText``, a regular
+``TextLine`` field:
+
+    >>> from zope.schema import TextLine
+    >>> field = TextLine(title=u'Some Title',
+    ...                  default=u'Default Value',
+    ...                  required=True)
+
+Now we can get a converter for this field by explicitly creating a
+:class:`TextConverter` instance:
+
+    >>> from waeup.kofa.utils.converters import TextConverter
+    >>> converter = TextConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.kofa.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.kofa.utils.converters.TextConverter object at 0x...>
+
+Now we can convert strings to this type:
+
+    >>> converter.fromString('blah')
+    u'blah'
+
+And back:
+
+    >>> converter.toString(u'blah')
+    'blah'
+
+Okay, not very surprising. But the field definitions can help also
+deliver values, if the given value is missing:
+
+    >>> converter.fromString(None)
+    u'Default Value'
+
+``None`` is not an acceptable value for fields which are required but
+provide no default:
+
+    >>> field = TextLine(title=u'Some Title',
+    ...                  required=True)
+
+    >>> converter = ISchemaTypeConverter(field)
+    >>> converter.fromString(None)
+    Traceback (most recent call last):
+    ...
+    RequiredMissing
+
+If we want to avoid this type of exception (and risk non-applicable
+data to be stored), we can use the ``strict`` parameter of
+``fromString()``:
+
+    >>> converter.fromString(None, strict=False) is None
+    True 
+
+
+If a field is not required, we will get the ``missing_value`` type in
+same case:
+
+    >>> field = TextLine(title=u'Some Title',
+    ...                  required=False)
+
+    >>> converter = ISchemaTypeConverter(field)
+    >>> converter.fromString(None) is None
+    True
+
+We can also set a value that indicates missing data (``None`` by default):
+
+    >>> field = TextLine(title=u'Some Title',
+    ...                  required=False,
+    ...                  missing_value=u'<NO-VALUE>')
+
+    >>> converter = ISchemaTypeConverter(field)
+    >>> converter.fromString(None)
+    u'<NO-VALUE>'
+
+And put it back:
+
+    >>> converter.toString(u'<NO-VALUE>') is None
+    True
+
+Bool field converters
+---------------------
+
+We create a field that also implements ``IBool``, a regular
+``Bool`` field:
+
+    >>> from zope.schema import Bool
+    >>> field = Bool(title=u'Some truth',
+    ...             default=True,
+    ...             required=True)
+
+Now we can get a converter for this field by explicitly creating a
+:class:`BoolConverter` instance:
+
+    >>> from waeup.kofa.utils.converters import BoolConverter
+    >>> converter = BoolConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.kofa.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.kofa.utils.converters.BoolConverter object at 0x...>
+
+Now we can convert strings to this type:
+
+    >>> converter.fromString('yes')
+    True
+
+    >>> converter.fromString('Yes')
+    True
+
+    >>> converter.fromString('yEs')
+    True
+
+    >>> converter.fromString('1')
+    True
+
+    >>> converter.fromString('True')
+    True
+
+    >>> converter.fromString('0')
+    False
+
+    >>> converter.fromString('no')
+    False
+
+    >>> converter.fromString('false')
+    False
+
+
+And back:
+
+    >>> converter.toString(True)
+    '1'
+
+    >>> converter.toString(False)
+    '0'
+
+Okay, not very surprising. But the field definitions can help also
+deliver values, if the given value is missing:
+
+    >>> converter.fromString(None)
+    True
+
+``None`` is not an acceptable value for fields which are required but
+provide no default:
+
+    >>> field = Bool(title=u'Some Truth',
+    ...             required=True)
+
+    >>> converter = ISchemaTypeConverter(field)
+    >>> converter.fromString(None)
+    Traceback (most recent call last):
+    ...
+    RequiredMissing
+
+If we want to avoid this type of exception (and risk non-applicable
+data to be stored), we can use the ``strict`` parameter of
+``fromString()``:
+
+    >>> converter.fromString(None, strict=False) is None
+    True
+
+The same for the inverse operation:
+
+    >>> converter.toString(None)
+    Traceback (most recent call last):
+    ...
+    RequiredMissing
+
+    >>> converter.toString(None, strict=False) is None
+    True
+
+
+If a field is not required, we will get the ``missing_value`` type in
+same case:
+
+    >>> field = Bool(title=u'Some Title',
+    ...             required=False)
+
+    >>> converter = ISchemaTypeConverter(field)
+    >>> converter.fromString(None) is None
+    True
+
+    >>> converter.toString(None) is None
+    True
+
+
+Int field converters
+--------------------
+
+We create a field that also implements ``IInt``, a regular
+``Int`` field:
+
+    >>> from zope.schema import Int
+    >>> field = Int(title=u'Some number',
+    ...             default=12,
+    ...             required=True)
+
+Now we can get a converter for this field by explicitly creating a
+:class:`IntConverter` instance:
+
+    >>> from waeup.kofa.utils.converters import IntConverter
+    >>> converter = IntConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.kofa.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.kofa.utils.converters.IntConverter object at 0x...>
+
+Now we can convert strings to this type:
+
+    >>> converter.fromString('666')
+    666
+
+And back:
+
+    >>> converter.toString(666)
+    '666'
+
+Okay, not very surprising. But the field definitions can help also
+deliver values, if the given value is missing:
+
+    >>> converter.fromString(None)
+    12
+
+``None`` is not an acceptable value for fields which are required but
+provide no default:
+
+    >>> field = Int(title=u'Some Title',
+    ...             required=True)
+
+    >>> converter = ISchemaTypeConverter(field)
+    >>> converter.fromString(None)
+    Traceback (most recent call last):
+    ...
+    RequiredMissing
+
+If we want to avoid this type of exception (and risk non-applicable
+data to be stored), we can use the ``strict`` parameter of
+``fromString()``:
+
+    >>> converter.fromString(None, strict=False) is None
+    True
+
+The same for the inverse operation:
+
+    >>> converter.toString(None)
+    Traceback (most recent call last):
+    ...
+    RequiredMissing
+
+    >>> converter.toString(None, strict=False) is None
+    True
+
+
+If a field is not required, we will get the ``missing_value`` type in
+same case:
+
+    >>> field = Int(title=u'Some Title',
+    ...             required=False)
+
+    >>> converter = ISchemaTypeConverter(field)
+    >>> converter.fromString(None) is None
+    True
+
+    >>> converter.toString(None) is None
+    True
+
+
+Choice field converters
+-----------------------
+
+Before choices really work in unit tests, we have to make sure, that
+the following adapters are registered (this is normally done during
+startup automatically):
+
+    >>> from zc.sourcefactory.browser.source import FactoredTerms
+    >>> from zc.sourcefactory.browser.token import (
+    ...   fromString, fromUnicode, fromInteger, fromPersistent)
+    >>> from zope.component import getGlobalSiteManager
+    >>> gsm = getGlobalSiteManager()
+    >>> gsm.registerAdapter(FactoredTerms)
+    >>> gsm.registerAdapter(fromString)
+    >>> gsm.registerAdapter(fromUnicode)
+    >>> gsm.registerAdapter(fromInteger)
+    >>> gsm.registerAdapter(fromPersistent)
+
+We create a field that also implements ``IChoice``, a regular
+``Choice`` field:
+
+    >>> from zope.schema import Choice
+    >>> field = Choice(title=u'Some number',
+    ...                default=u'a',
+    ...                values=['a', 'b', 'c'],
+    ...                required=True)
+
+Now we can get a converter for this field by explicitly creating a
+:class:`ChoiceConverter` instance:
+
+    >>> from waeup.kofa.utils.converters import ChoiceConverter
+    >>> converter = ChoiceConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.kofa.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.kofa.utils.converters.ChoiceConverter object at 0x...>
+
+Now we can convert strings to this type:
+
+    >>> converter.fromString('a')
+    'a'
+
+or values back to strings:
+
+    >>> converter.toString('a')
+    'a'
+
+Non-listed values will not be accepted:
+
+    >>> converter.fromString('nonsense')
+    Traceback (most recent call last):
+    ...
+    LookupError: nonsense
+
+Also non-string values will work:
+
+    >>> from zope.schema import Choice
+    >>> field = Choice(title=u'Some number',
+    ...                default=2,
+    ...                values=[1, 2, 3],
+    ...                required=True)
+
+    >>> converter = ISchemaTypeConverter(field)
+    >>> converter.fromString('1')
+    1
+
+If we define an own source, then it will cause no problems:
+
+    >>> from zc.sourcefactory.basic import BasicSourceFactory
+    >>> class CustomSource(BasicSourceFactory):
+    ...   def getValues(self):
+    ...     return [1,2,3]
+    ...    
+    ...   def getTitle(self, value):
+    ...     return "Number %s" % (value,)
+
+    >>> field = Choice(title=u'Some number',
+    ...                default=2,
+    ...                source=CustomSource(),
+    ...                required=True)
+
+    >>> converter = ISchemaTypeConverter(field)
+    >>> converter.fromString('1')
+    1
+
+Note, that when asking for conversion of Choice fields, you must
+deliver the token value of the source/vocabulary. This might be the
+same as the actual value as string, but it it might be different,
+depending of the kind of source/vocabulary used.
+
+Furthermore, when writing 'exporters', i.e. components that turn a
+value into a string for usage in CSV files, then you have to save the
+token value of a term.
+
+Using the :meth:`toString()` method will deliver the token for us:
+
+    >>> converter.toString(1)
+    '1'
+
+    >>> from waeup.kofa.interfaces import SimpleKofaVocabulary
+    >>> field = Choice(
+    ...   title = u'Favourite Dish',
+    ...   default = 0,
+    ...   vocabulary = SimpleKofaVocabulary(
+    ...        ('N/A', 0), ('Pizza', 1),
+    ...        ('Cake', 2), ('Muffins', 3)),
+    ...   required = True,
+    ...   )
+
+    >>> converter = ISchemaTypeConverter(field)
+    >>> converter.fromString('0')
+    0
+
+    >>> converter.toString(1)
+    '1'
+
+A more complex (but still realistic example) for conversion of
+:mod:`zope.schema.Choice` fields follows. Here we have a special kind
+of object, the `Cave` class, and get their `name` attribute as token.
+
+    >>> class Cave(object):
+    ...   name = 'Home sweet home'
+
+    >>> def tokenizer(cave):
+    ...   return cave.name
+
+The tokenizer has to be registered as an Cave-to-IToken adapter to
+keep :mod:`zc.sourcefactory` happy:
+
+    >>> from zc.sourcefactory.interfaces import IToken
+    >>> from zope.component import getGlobalSiteManager
+    >>> gsm = getGlobalSiteManager()
+    >>> gsm.registerAdapter(tokenizer, required=(Cave,), provided=IToken)
+
+Now we finally can create two objects of `Cave` and make a field, that
+gives the choice between these two objects:
+
+    >>> obj1 = Cave()
+    >>> obj1.name = 'Wilma'
+
+    >>> obj2 = Cave()
+    >>> obj2.name = 'Fred'
+
+    >>> from zc.sourcefactory.basic import BasicSourceFactory
+    >>> class CustomSource(BasicSourceFactory):
+    ...   def getValues(self):
+    ...     return [obj1, obj2]
+    ...    
+    ...   def getTitle(self, value):
+    ...     return value.name
+
+    >>> field = Choice(title=u'Some cave',
+    ...                default=obj1,
+    ...                source=CustomSource(),
+    ...                required=True)
+
+We get a converter for this field in the usual way. The
+:meth:`waeup.kofa.utils.converters.fromString()` method will return one of
+the objects if we feed it with ``'Wilma'`` or ``'Fred'``:
+
+    >>> converter = ISchemaTypeConverter(field)
+    >>> result = converter.fromString('Wilma')
+    >>> result
+    <Cave object at 0x...>
+
+    >>> result is obj1, result.name
+    (True, 'Wilma')
+
+If we use the converter the other way round, it will call the
+tokenizer above and deliver ``'Wilma'`` or ``'Fred'`` as they are the
+tokens of the objects in question:
+
+    >>> converter.toString(obj2)
+    'Fred'
+
+Date field converters
+---------------------
+
+We create a field that also implements ``IDate``, a regular
+``Date`` field:
+
+    >>> from zope.schema import Date
+    >>> from datetime import date
+    >>> field = Date(title=u'Some date',
+    ...             default=date(2010, 4, 1),
+    ...             required=True)
+
+Now we can get a converter for this field by explicitly creating a
+:class:`IntConverter` instance:
+
+    >>> from waeup.kofa.utils.converters import DateConverter
+    >>> converter = DateConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.kofa.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.kofa.utils.converters.DateConverter object at 0x...>
+
+Now we can convert strings to this type:
+
+    >>> converter.fromString('2007-04-23')
+    datetime.date(2007, 4, 23)
+
+And back:
+
+    >>> converter.toString(date(2007,4,23))
+    '2007-04-23'
+
+As string input we also accept 'DD/MM/YYYY':
+
+    >>> converter.fromString('13/05/1945')
+    datetime.date(1945, 5, 13)
+
+and some more bizarre formats:
+
+    >>> converter.fromString('1945/05/13')
+    datetime.date(1945, 5, 13)
+
+    >>> converter.fromString('1945/05/13 12:12:34 GMT+1')
+    datetime.date(1945, 5, 13)
+
+    >>> converter.fromString('1945/5/6')
+    datetime.date(1945, 5, 6)
+
+    >>> converter.fromString('06.05.1945')
+    datetime.date(1945, 5, 6)
+
+    >>> converter.fromString('6.5.1945')
+    datetime.date(1945, 5, 6)
+
+while other date formats will mean trouble:
+
+    >>> converter.fromString('first of July 1960')
+    Traceback (most recent call last):
+    ...
+    ValueError: Cannot convert to date: first of July 1960. Use YYYY-MM-DD.
+
+Okay, not very surprising. But the field definitions can help also
+deliver values, if the given value is missing:
+
+    >>> converter.fromString(None)
+    datetime.date(2010, 4, 1)
+
+``None`` is not an acceptable value for fields which are required but
+provide no default:
+
+    >>> field = Date(title=u'Some Title',
+    ...             required=True)
+    >>> converter = ISchemaTypeConverter(field)
+    >>> converter.fromString(None)
+    Traceback (most recent call last):
+    ...
+    RequiredMissing
+
+If we want to avoid this type of exception (and risk non-applicable
+data to be stored), we can use the ``strict`` parameter of
+``fromString()``:
+
+    >>> converter.fromString(None, strict=False) is None
+    True
+
+The same for the inverse operation:
+
+    >>> converter.toString(None)
+    Traceback (most recent call last):
+    ...
+    RequiredMissing
+    >>> converter.toString(None, strict=False) is None
+    True
+
+
+If a field is not required, we will get the ``missing_value`` type in
+same case:
+
+    >>> field = Date(title=u'Some Title',
+    ...             required=False)
+    >>> converter = ISchemaTypeConverter(field)
+    >>> converter.fromString(None) is None
+    True
+
+    >>> converter.toString(None) is None
+    True
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/countries.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/countries.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/countries.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/utils/countries.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/countries.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/countries.txt	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/utils/country_helpers.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/country_helpers.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/country_helpers.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/utils/helpers.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/helpers.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/helpers.py	(revision 10886)
@@ -0,0 +1,756 @@
+## $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 grok
+from cStringIO import StringIO
+from docutils.core import publish_string
+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.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
+
+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=[]):
+    """Turn the attributes of a class 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
+        setattr(cls, field_name, FieldProperty(iface[field_name]))
+    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 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`, 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.
+
+    `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).
+    """
+    linenum = 1
+    reader = csv.DictReader(iterable)
+    try:
+        for row in reader:
+            linenum += 1
+    except UnicodeDecodeError:
+        return linenum
+    except:
+        return linenum + 1
+    return None
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/helpers.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/helpers.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/helpers.txt	(revision 10886)
@@ -0,0 +1,228 @@
+:mod:`waeup.kofa.utils.helpers` -- 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/tags/0.2/src/waeup/kofa/utils/importexport.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/importexport.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/importexport.py	(revision 10886)
@@ -0,0 +1,101 @@
+## $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 unicodecsv as csv # XXX: csv ops should move to dedicated module
+import grok
+import cPickle
+import zope.xmlpickle
+from cStringIO import StringIO
+from zope.interface import Interface
+from waeup.kofa.interfaces import (IKofaObject, IKofaExporter,
+                                   IKofaXMLExporter, IKofaXMLImporter)
+
+def readFile(f):
+    """read a CSV file"""
+    headers = []
+    rows = []
+    f = csv.reader(f)
+    headers = f.next()
+    for line in f:
+        rows.append(line)
+    return (headers, rows)
+
+def writeFile(data):
+    writer = csv.writer(open("export.csv", "wb"))
+    writer.writerows(data)
+
+class Exporter(grok.Adapter):
+    """Export a Kofa object as pickle.
+
+    This all-purpose exporter exports attributes defined in schemata
+    and contained objects (if the exported thing is a container).
+    """
+    grok.context(IKofaObject)
+    grok.provides(IKofaExporter)
+
+    def __init__(self, context):
+        self.context = context
+
+    def export(self, filepath=None):
+        if filepath is None:
+            filelike_obj = StringIO()
+        else:
+            filelike_obj = open(filepath, 'wb')
+        exported_obj = cPickle.dump(self.context, filelike_obj)
+        filelike_obj.close()
+        return filelike_obj
+
+class XMLExporter(grok.Adapter):
+    """Export a Kofa object as XML.
+
+    This all-purpose exporter exports XML representations of pickable
+    objects.
+    """
+    grok.context(Interface)
+    grok.provides(IKofaXMLExporter)
+
+    def __init__(self, context):
+        self.context = context
+
+    def export(self, filepath=None):
+        pickled_obj = cPickle.dumps(self.context)
+        result = zope.xmlpickle.toxml(pickled_obj)
+        if filepath is None:
+            filelike_obj = StringIO()
+        else:
+            filelike_obj = open(filepath, 'wb')
+        filelike_obj.write(result)
+        filelike_obj.seek(0)
+        return filelike_obj
+
+class XMLImporter(grok.Adapter):
+    """Import a Kofa object from XML.
+    """
+    grok.context(Interface)
+    grok.provides(IKofaXMLImporter)
+
+    def __init__(self, context):
+        self.context = context
+
+    def doImport(self, filepath):
+        xml = None
+        if isinstance(filepath, basestring):
+            xml = open(filepath, 'rb').read()
+        else:
+            xml = filepath.read()
+        obj = zope.xmlpickle.loads(xml)
+        return obj
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/importexport.txt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/importexport.txt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/importexport.txt	(revision 10886)
@@ -0,0 +1,145 @@
+Importing and Exporting Things
+******************************
+
+The waeup package tries to support all special object types to be
+exportable and importable.
+
+.. :doctest:
+.. :layer: waeup.kofa.testing.KofaUnitTestLayer
+
+As imports and exports of data are a cruical point when doing updates
+or to enable data backups, this support must be considered a serious
+topic for the base functionality of the whole software.
+
+Exports and imports are implemented using the Zope component framework
+capabilities. This means we need some adapters and utilities, which we
+register, by grokking the whole package. This is done on startup
+automatically, but in doctests (like this one) it is done by the
+testlayer.
+
+Furthermore we need something to import/export. As only pickable
+classes are importable/exportable and these have to be defined in some
+globally reachable module, we first define some silly object in
+filesystem and adjust the path to lookup modules:
+
+    >>> import os
+    >>> import sys
+    >>> os.mkdir('stoneage')
+    >>> old_sys_path = sys.path
+    >>> open(os.path.join('stoneage', '__init__.py'), 'wb').write(
+    ... """
+    ... class Cave(object):
+    ...   name = 'Unnamed cave'
+    ...   dinoports = 2
+    ... """)
+    >>> sys.path.append(os.getcwd())
+
+Now we can import stoneage.Cave objects.
+
+
+Exporting XML
+=============
+
+One of the major formats we support for data export is XML.
+
+When we want to export an object, we can choose the destination
+file. If we do not give a file to write to, we get a StringIO object,
+i.e. an in-memory file.
+
+We can export caves. First we create a cave:
+
+    >>> from stoneage import Cave
+    >>> mycave = Cave()
+    >>> mycave.name = 'Freds Cave'
+    >>> mycave.dinoports = 1
+
+Exporting plain XML
+-------------------
+
+Now let's export it to XML. To do this we first ask the framework for
+an exporter::
+
+    >>> from waeup.kofa.interfaces import IKofaXMLExporter
+    >>> exporter = IKofaXMLExporter(mycave)
+    >>> exporter
+    <waeup.kofa.utils.importexport.XMLExporter object at 0x...>
+
+All exporters provide an ``export(<obj>)`` method::
+
+    >>> result = exporter.export()
+
+The result is an object of type StringIO. We therefore use its
+`read()` method to get the actual XML data::
+
+    >>> print result.read()
+    <?xml version="1.0" encoding="utf-8" ?>...
+
+
+Generating XML files
+--------------------
+
+We can also let the results be written to a file::
+
+    >>> result = exporter.export('myexport.xml')
+    >>> print open('myexport.xml', 'rb').read()
+    <?xml version="1.0" encoding="utf-8" ?>
+    <pickle>
+      <object>
+    ...
+    </pickle>
+
+
+Importing XML
+=============
+
+We can generate objects from XML.
+
+We change the name of the cave:
+
+    >>> mycave.name = 'Another name'
+
+Now we create an importer for that object:
+
+    >>> from waeup.kofa.interfaces import IKofaXMLImporter
+    >>> importer = IKofaXMLImporter(mycave)
+
+Importing from filenames
+------------------------
+
+We can use a filepath to import from the denoted file::
+
+    >>> new_obj = importer.doImport('myexport.xml')
+
+The object created is indeed a cave::
+
+    >>> new_obj
+    <stoneage.Cave object at 0x...>
+
+    >>> new_obj.name
+    'Freds Cave'
+
+The new object is really a new object::
+
+    >>> new_obj is mycave
+    False
+
+Importing from filelike objects
+-------------------------------
+
+We can also pass a file-like object instead of a filepath::
+
+    >>> filelike_obj = open('myexport.xml', 'rb')
+    >>> new_obj = importer.doImport(filelike_obj)
+
+The object created is indeed a faculty::
+
+    >>> new_obj
+    <stoneage.Cave object at 0x...>
+
+Clean up::
+
+    >>> import os
+    >>> import shutil
+    >>> sys.path = old_sys_path
+    >>> os.unlink('myexport.xml')
+    >>> shutil.rmtree('stoneage')
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/logger.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/logger.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/logger.py	(revision 10886)
@@ -0,0 +1,398 @@
+## $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 ulif.loghandlers import DatedRotatingFileHandler
+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 logfile size (5 KB, not relevant for DatedRotatingFileHandlers)
+MAX_BYTES = 5 * 1024 ** 2
+
+#: Default num of backup files (-1 = indefinite)
+BACKUP_COUNT = -1
+
+#: 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 = DatedRotatingFileHandler(
+        #    filename, when='MON', backupCount=BACKUP_COUNT)
+        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/tags/0.2/src/waeup/kofa/utils/overrides.zcml
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/overrides.zcml	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/overrides.zcml	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/utils/tests/test_batching.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/tests/test_batching.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/tests/test_batching.py	(revision 10886)
@@ -0,0 +1,615 @@
+## $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()
+        writer, outfile = exporter.get_csv_writer(filepath=self.workfile)
+        self.assertRaises(NotImplementedError, exporter.export_filtered,
+                          'foo', bar='bar')
+        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
+
+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
+
+
+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/tags/0.2/src/waeup/kofa/utils/tests/test_browser.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/tests/test_browser.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/tests/test_browser.py	(revision 10886)
@@ -0,0 +1,139 @@
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Test the student-related UI components.
+"""
+import shutil
+import tempfile
+import pytz
+from datetime import datetime, timedelta
+from StringIO import StringIO
+import os
+import grok
+from zope.event import notify
+from zope.component import createObject, queryUtility
+from zope.component.hooks import setSite, clearSite
+from zope.catalog.interfaces import ICatalog
+from zope.security.interfaces import Unauthorized
+from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
+
+from waeup.kofa.utils.browser import replaceStudentMessages
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+from waeup.kofa.applicants.tests.test_browser import ApplicantsFullSetup
+
+class StudentUtilsUITests(StudentsFullSetup):
+
+    layer = FunctionalLayer
+
+    def test_replace_student_messages(self):
+        self.assertTrue('Record created by system' in
+            self.student.history.messages[0])
+        replaceStudentMessages('system', 'me')
+        self.assertTrue('Record created by me' in
+            self.student.history.messages[0])
+
+    def test_modify_all_student_history(self):
+        self.assertTrue('Record created by system' in
+            self.student.history.messages[0])
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/modify_student_history')
+        self.assertTrue(
+            'Syntax: /modify_student_history?old=[old string]&new=[new string]'
+            in self.browser.contents)
+        self.browser.open(
+            'http://localhost/app/modify_student_history?old=by system&new=by me')
+        self.assertTrue('Finished' in self.browser.contents)
+        self.assertTrue('Record created by me' in
+            self.student.history.messages[0])
+
+    def test_remove_student_history_message(self):
+        self.assertTrue('Record created by system' in
+            self.student.history.messages[0])
+        self.assertEqual(len(self.student.history.messages),1)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/remove_student_history_message')
+        self.assertTrue(
+            'Syntax: /remove_student_history_message?student_id=[id]&number=[line number, starting with 0]'
+            in self.browser.contents)
+        self.browser.open(
+            'http://localhost/app/remove_student_history_message?student_id=%s&number=0' % self.student.student_id)
+        self.assertTrue('Finished' in self.browser.contents)
+        self.assertEqual(len(self.student.history.messages),0)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertMatches(
+            "...zope.mgr - line '<YYYY-MM-DD hh:mm:ss> UTC - "
+            "Record created by system' removed in K1000000 history",
+            logcontent)
+
+    def test_reindex(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/reindex')
+        self.assertTrue('No catalog name provided' in self.browser.contents)
+        self.browser.open('http://localhost/app/reindex?ctlg=xyz')
+        self.assertTrue('xyz_catalog does not exist' in self.browser.contents)
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(student_id=(None, None))
+        self.assertEqual(len(results),1)
+        cat.clear()
+        results = cat.searchResults(student_id=(None, None))
+        self.assertEqual(len(results),0)
+        self.browser.open('http://localhost/app/reindex?ctlg=students')
+        self.assertTrue('1 students re-indexed' in self.browser.contents)
+        results = cat.searchResults(student_id=(None, None))
+        self.assertEqual(len(results),1)
+
+class ApplicantUtilsUITests(ApplicantsFullSetup):
+
+    layer = FunctionalLayer
+
+    def test_modify_all_applicant_history(self):
+        self.assertTrue('Application initialized by system' in
+            self.applicant.history.messages[0])
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/modify_applicant_history')
+        self.assertTrue(
+            'Syntax: /modify_applicant_history?old=[old string]&new=[new string]'
+            in self.browser.contents)
+        self.browser.open(
+            'http://localhost/app/modify_applicant_history?old=by system&new=by me')
+        self.assertTrue('Finished' in self.browser.contents)
+        self.assertTrue('Application initialized by me' in
+            self.applicant.history.messages[0])
+
+    def test_remove_applicant_history_message(self):
+        self.assertTrue('Application initialized by system' in
+            self.applicant.history.messages[0])
+        self.assertEqual(len(self.applicant.history.messages),1)
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/remove_applicant_history_message')
+        self.assertTrue(
+            'Syntax: /remove_applicant_history_message?applicant_id=[id]&number=[line number, starting with 0]'
+            in self.browser.contents)
+        self.browser.open(
+            'http://localhost/app/remove_applicant_history_message?applicant_id=%s&number=0' % self.applicant.applicant_id)
+        self.assertTrue('Finished' in self.browser.contents)
+        self.assertEqual(len(self.applicant.history.messages),0)
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertMatches(
+            "...zope.mgr - line '<YYYY-MM-DD hh:mm:ss> UTC - "
+            "Application initialized by system' removed in %s history"
+            % self.applicant.applicant_id, logcontent)
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/tests/test_converters.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/tests/test_converters.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/tests/test_converters.py	(revision 10886)
@@ -0,0 +1,428 @@
+## $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.university import Faculty
+from waeup.kofa.utils.converters import (
+    IObjectConverter, IFieldConverter, 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,
+        default = [],
+        )
+    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 ContactFactory(object):
+    """A factory for faculty containers.
+    """
+    implements(IContact)
+
+    def __call__(self, *args, **kw):
+        return Faculty()
+
+    def getInterfaces(self):
+        return implementedBy(Faculty)
+
+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_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/tags/0.2/src/waeup/kofa/utils/tests/test_doctests.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/tests/test_doctests.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/tests/test_doctests.py	(revision 10886)
@@ -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(['utils/batching.txt'])
+    return suite
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/tests/test_helpers.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/tests/test_helpers.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/tests/test_helpers.py	(revision 10886)
@@ -0,0 +1,558 @@
+# -*- 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()
+        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
+
+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_suite():
+    suite = unittest.TestSuite()
+    # Register local test cases...
+    for testcase in [
+        ReST2HTMLTestCase,
+        FactoryBaseTestCase,
+        CopyFileSystemTreeTestCase,
+        RemoveFileOrDirectoryTestCase,
+        CurrentPrincipalTestCase,
+        CmpFilesTestCase,
+        FileSizeTestCase,
+        IfaceNamesTestCase,
+        DateTimeHelpersTestCase,
+        GetFileFormatTestCase,
+        MergeCSVFileTestCase,
+        SimpleHelpersTestCase,
+        CheckCSVCharsetTestCase,
+        ]:
+        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/tags/0.2/src/waeup/kofa/utils/tests/test_logger.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/tests/test_logger.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/tests/test_logger.py	(revision 10886)
@@ -0,0 +1,426 @@
+## $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, MAX_BYTES, BACKUP_COUNT, 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]
+        if hasattr(handler, 'maxBytes'):
+            self.assertEqual(handler.maxBytes, MAX_BYTES)
+        if hasattr(handler, 'backupCount'):
+            self.assertEqual(handler.backupCount, BACKUP_COUNT)
+        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/tags/0.2/src/waeup/kofa/utils/utils.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/utils.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/utils/utils.py	(revision 10886)
@@ -0,0 +1,285 @@
+## $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 os
+import grok
+import string
+import pytz
+from copy import deepcopy
+from random import SystemRandom as r
+from zope.i18n import translate
+from zope.interface import implements
+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.countries import COUNTRIES
+from waeup.kofa.utils.helpers import get_sorted_preferred
+
+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).
+    PORTAL_LANGUAGE = 'en'
+
+    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',
+        }
+
+    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'
+        }
+
+    SPECIAL_HANDLING_DICT = {
+        'regular': 'Regular Hostel',
+        'blocked': 'Blocked Hostel',
+        'pg': 'Postgraduate Hostel'
+        }
+
+    SPECIAL_APP_DICT = {
+        'transcript': 'Transcript Fee Payment',
+        }
+
+    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',
+        'transcript': 'Transcript Fee',
+        }
+
+    SELECTABLE_PAYMENT_CATEGORIES = deepcopy(PAYMENT_CATEGORIES)
+
+    PREVIOUS_PAYMENT_CATEGORIES = deepcopy(SELECTABLE_PAYMENT_CATEGORIES)
+
+    BALANCE_PAYMENT_CATEGORIES = {
+        'schoolfee': 'School 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',),
+        }
+
+    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)
+
+    @property
+    def tzinfo(self):
+        # For Nigeria: pytz.timezone('Africa/Lagos')
+        # For Germany: pytz.timezone('Europe/Berlin')
+        return pytz.utc
+
+    def fullname(self,firstname,lastname,middlename=None):
+        """Full name constructor.
+        """
+        # We do not necessarily have the middlename attribute
+        if middlename:
+            name = '%s %s %s' % (firstname, middlename, lastname)
+        else:
+            name = '%s %s' % (firstname, lastname)
+        return string.capwords(name.replace('-',' - ')).replace(' - ','-')
+
+
+    def genPassword(self, length=8, chars=string.letters + string.digits):
+        """Generate a random password.
+        """
+        return ''.join([r().choice(chars) for i in range(length)])
+
+
+    def sendCredentials(self, user, password=None, url_info=None, msg=None):
+        """Send credentials as email.
+
+        Input is the applicant for which credentials are sent and the
+        password.
+
+        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 getPaymentItem(self, payment):
+        """Return payment item.
+
+        This method can be used to customize the display_item property method.
+        """
+        return payment.p_item
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/webservices.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/webservices.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/webservices.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/widgets/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/__init__.py	(revision 10886)
@@ -0,0 +1,2 @@
+"""JS-driven widgets for Kofa.
+"""
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/datetimewidget.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/datetimewidget.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/datetimewidget.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/widgets/datewidget.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/datewidget.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/datewidget.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/widgets/htmlwidget.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/htmlwidget.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/htmlwidget.py	(revision 10886)
@@ -0,0 +1,58 @@
+## $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
+##
+"""A widget that renders localized html.
+"""
+from zope.component import getUtility
+from zope.formlib.widget import renderElement, DisplayWidget
+from waeup.kofa.interfaces import IKofaUtils
+
+
+class HTMLDisplayWidget(DisplayWidget):
+    """Restructured Text widget.
+    """
+
+    def __call__(self):
+        """The HTMLDisplayWidget transforms an localizez 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.
+        The latter can be configured in waeup.srp.utils.utils.KofaUtils.
+        """
+        if self._renderedValueSet():
+            value = self._data
+        else:
+            value = self.context.default
+        if value == self.context.missing_value:
+            return {}
+        parts = value.split('>>')
+        elements = {}
+        lang = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        for part in parts:
+            if part[2:4] == u'<<':
+                lang = 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
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/interfaces.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/interfaces.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/interfaces.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/widgets/objectwidget.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/objectwidget.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/objectwidget.pt	(revision 10886)
@@ -0,0 +1,5 @@
+<span tal:repeat="widget context/subwidgets">
+  <span tal:define="error widget/error"
+    tal:replace="structure error" tal:condition="error" />
+  <span tal:replace="structure widget" />
+</span>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/objectwidget.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/objectwidget.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/objectwidget.py	(revision 10886)
@@ -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
+##
+"""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):
+
+    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)
+
+    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/tags/0.2/src/waeup/kofa/widgets/overrides.zcml
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/overrides.zcml	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/overrides.zcml	(revision 10886)
@@ -0,0 +1,45 @@
+<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"
+      />
+
+</configure>
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/phonewidget.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/phonewidget.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/phonewidget.py	(revision 10886)
@@ -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')
+    bootstrap_span = 'span2'
+
+    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="span4">\n%s\n</select>' % (
+            '%s.%s' % (self.name, 'country'),
+            '%s.%s' % (self.name, 'country'),
+            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, 'span1'))
+            kwargs.update(cssClass = '%s %s' % ('', self.bootstrap_span))
+            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/tags/0.2/src/waeup/kofa/widgets/restwidget.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/restwidget.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/restwidget.py	(revision 10886)
@@ -0,0 +1,59 @@
+## $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 that renders restructured text.
+"""
+from zope.component import getUtility
+from zope.formlib.widget import renderElement, DisplayWidget
+from waeup.kofa.utils.helpers import ReST2HTML
+from waeup.kofa.interfaces import IKofaUtils
+
+
+class ReSTDisplayWidget(DisplayWidget):
+    """Restructured Text widget.
+    """
+
+    def __call__(self):
+        """The ReSTDisplayWidget transforms a 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.
+        The latter can be configured in waeup.srp.utils.utils.KofaUtils.
+        """
+        if self._renderedValueSet():
+            value = self._data
+        else:
+            value = self.context.default
+        if value == self.context.missing_value:
+            return {}
+        parts = value.split('>>')
+        elements = {}
+        lang = getUtility(IKofaUtils).PORTAL_LANGUAGE
+        for part in parts:
+            if part[2:4] == u'<<':
+                lang = 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
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/sequencewidget.pt
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/sequencewidget.pt	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/sequencewidget.pt	(revision 10886)
@@ -0,0 +1,34 @@
+<table border="0" class="sequencewidget"
+  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>
+      <span tal:define="error widget/error"
+            tal:replace="structure error" tal:condition="error" />
+      <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>
+      <input type="submit" value="Remove selected items"
+             class="btn"
+             tal:condition="view/need_delete"
+             tal:attributes="name string:${view/name}.remove"
+             i18n:attributes="value remove-selected-items" />
+      <input type="submit" value="Add foo"
+             class="btn"
+             tal:condition="view/need_add"
+             tal:attributes="name string:${view/name}.add;
+                             value view/addButtonLabel" />
+    </td>
+  </tr>
+</table>
+<input tal:replace="structure view/marker" />
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/sequencewidget.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/sequencewidget.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/sequencewidget.py	(revision 10886)
@@ -0,0 +1,96 @@
+## $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')
+
+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/tags/0.2/src/waeup/kofa/widgets/tests/__init__.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/tests/__init__.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/tests/__init__.py	(revision 10886)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/tests/test_datetimewidget.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/tests/test_datetimewidget.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/tests/test_datetimewidget.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/widgets/tests/test_datewidget.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/tests/test_datewidget.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/tests/test_datewidget.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/widgets/tests/test_objectwidget.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/tests/test_objectwidget.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/tests/test_objectwidget.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/widgets/tests/test_phonewidget.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/tests/test_phonewidget.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/widgets/tests/test_phonewidget.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/workflow.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/workflow.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/workflow.py	(revision 10886)
@@ -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/tags/0.2/src/waeup/kofa/zcml.py
===================================================================
--- main/waeup.kofa/tags/0.2/src/waeup/kofa/zcml.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/src/waeup/kofa/zcml.py	(revision 10886)
@@ -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/tags/0.2/tools/add_old_id_col.py
===================================================================
--- main/waeup.kofa/tags/0.2/tools/add_old_id_col.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/tools/add_old_id_col.py	(revision 10886)
@@ -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/tags/0.2/tools/copy_media_files.py
===================================================================
--- main/waeup.kofa/tags/0.2/tools/copy_media_files.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/tools/copy_media_files.py	(revision 10886)
@@ -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/tags/0.2/tools/download_exports.sh
===================================================================
--- main/waeup.kofa/tags/0.2/tools/download_exports.sh	(revision 10886)
+++ main/waeup.kofa/tags/0.2/tools/download_exports.sh	(revision 10886)
@@ -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/tags/0.2/tools/fix_import_file.py
===================================================================
--- main/waeup.kofa/tags/0.2/tools/fix_import_file.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/tools/fix_import_file.py	(revision 10886)
@@ -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/tags/0.2/tools/prepare_jamb_file.py
===================================================================
--- main/waeup.kofa/tags/0.2/tools/prepare_jamb_file.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/tools/prepare_jamb_file.py	(revision 10886)
@@ -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/tags/0.2/tools/reload_portraits.py
===================================================================
--- main/waeup.kofa/tags/0.2/tools/reload_portraits.py	(revision 10886)
+++ main/waeup.kofa/tags/0.2/tools/reload_portraits.py	(revision 10886)
@@ -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/tags/0.2/versions.cfg
===================================================================
--- main/waeup.kofa/tags/0.2/versions.cfg	(revision 10886)
+++ main/waeup.kofa/tags/0.2/versions.cfg	(revision 10886)
@@ -0,0 +1,129 @@
+# Versions of packages used by waeup.kofa
+[buildout]
+# default set of versions we use.
+extends= http://grok.zope.org/releaseinfo/grok-1.1.cfg
+
+versions = versions
+
+[versions]
+collective.recipe.sphinxbuilder = 0.7.0
+hurry.workflow = 0.11
+# Pinned to circumvent breakage in 0.4.x
+hurry.zoperesource = 0.6
+# Pinned to prevent buildout svn-error.
+lovely.recipe = 1.0.0
+megrok.layout = 1.0.2
+reportlab = 2.5
+transaction = 1.1.0
+z3c.testsetup = 0.6.1
+zc.buildout = 2.1.0
+zc.recipe.egg = 2.0.0a3
+zc.recipe.testrunner = 2.0.0
+zope.app.testing = 3.8.1
+# for support of @provider directive
+zope.interface = 3.6.3
+# for support of contextual default values
+zope.schema = 3.8.0
+zope.testing = 3.10.2
+zope.xmlpickle = 3.4.0
+# Require latest version...
+Sphinx = 1.0.7
+ZODB3 = 3.10.3
+docutils = 0.7
+Jinja2 = 2.3
+# for support of unicode encoded passwords
+zope.password = 3.6.1
+# include bugfix for hanging xmlrpcrequests under paster
+zope.publisher = 3.12.2
+# Pillow >= 2.0 supports Python 2.6, 2.7, 3.x
+# Pillow < 1.x supports Python 2.4, 2.5, 2.6, 2.7
+Pillow = 2.0.0
+mock = 1.0.1
+
+# Added by buildout at 2013-04-29 11:28:31.957221
+Beaker = 1.6.4
+Twisted = 13.0.0
+dolmen.beaker = 0.1
+hurry.resource = 0.10
+repoze.profile = 2.0
+repoze.sphinx.autointerface = 0.7.1
+unittest2 = 0.5.1
+# z3c.coverage 2.0 is out but does not cope with non-ASCII in sources
+z3c.coverage = 1.3.1
+zc.async = 1.5.4
+zc.dict = 1.2.1
+zc.twist = 1.3.1
+zc.z3monitor = 0.8.0
+zope.bforest = 1.2
+
+# Required by:
+# z3c.coverage==2.0.0
+coverage = 3.6
+
+# Required by:
+# waeup.kofa==0.2dev
+hurry.file = 1.2.1
+
+# Required by:
+# waeup.kofa==0.2dev
+hurry.jquery = 1.4.3.1
+
+# Required by:
+# waeup.kofa==0.2dev
+hurry.jqueryui = 1.8.5.1
+
+# Required by:
+# waeup.kofa==0.2dev
+hurry.query = 1.1.1
+
+# Required by:
+# grokui.base==0.2
+megrok.menu = 0.4
+
+# Required by:
+# dolmen.beaker==0.1
+pycryptopp = 0.6.0.1206569328141510525648634803928199668821045408958
+
+# Required by:
+# repoze.profile==2.0
+pyprof2calltree = 1.1.0
+
+# Required by:
+# zc.async==1.5.4
+rwproperty = 1.0
+
+# Required by:
+# zope.testrunner==4.3.3
+six = 1.3.0
+
+# Required by:
+# waeup.kofa==0.2dev
+ulif.loghandlers = 0.1.1
+
+# Required by:
+# waeup.kofa==0.2dev
+unicodecsv = 0.9.4
+
+# Required by:
+# zc.async==1.5.4
+uuid = 1.30
+
+# Required by:
+# zc.z3monitor==0.8.0
+zc.monitor = 0.3.1
+
+# Required by:
+# zc.monitor==0.3.1
+zc.ngi = 2.0.1
+
+# Required by:
+# zc.async==1.5.4
+zc.queue = 1.3
+
+# Required by:
+# waeup.kofa==0.2dev
+zope.errorview = 0.11
+
+# Required by:
+# zc.recipe.testrunner==2.0.0
+zope.testrunner = 4.3.3
