Index: main/waeup.sirp/branches/ulif-schoolgrades/CHANGES.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/CHANGES.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/CHANGES.txt	(revision 7760)
@@ -0,0 +1,47 @@
+Changes
+*******
+
+0.2 (unreleased)
+================
+
+Feature changes
+---------------
+
+* Started switch to groktoolkit (grok 1.1) based
+  application. `waeup.sirp` is not compatible with grok 1.0 anymore.
+
+* Added first rudimentary support for application plugins. Plugins are
+  looked up by the main app on creation time and created afterwards.
+
+* Added first rudimentary support for RSS2.0 feeds.
+
+* Generated filenames now have a filename extension that indicates
+  mode of processing. So fred.csv becomes fred.create.pending.csv when
+  being batch processed and something fails.
+
+* Coverage reports are now generated in ``coverage-report`` subdir of
+  build-directory.
+
+Bug fixes
+---------
+
+* Removed all references to stuff in university submodule into the
+  module itself or replaced it with proper code. This way the
+  university submodule could be factored out of the waeup.sirp
+  package. Fixed bug #23.
+
+* Fixed default obj updater in batchprocessing. Fixed bug #17.
+
+* Fixed handling of pending file names, fixed bug #16.
+
+* Removed quick importer and all related components.
+
+* Removed TestData folder. It was too heavy and test data can be
+  created on-the-fly.
+
+
+0.1 (2010-01-31)
+================
+
+Initial version. (Incomplete) rewrite from the former CPS-based
+application.
Index: main/waeup.sirp/branches/ulif-schoolgrades/LICENSE
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/LICENSE	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/LICENSE	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/README.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/README.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/README.txt	(revision 7760)
@@ -0,0 +1,10 @@
+What is WAeUP.SIRP ?
+********************
+
+The WAeUP Student Information and Registration Portal (WAeUP.SIRP) is
+a web-based management tool for managing universities and schools.
+
+The package was created as an effort of the WAeUP group. 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.sirp/branches/ulif-schoolgrades/buildout-windows.cfg
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/buildout-windows.cfg	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/buildout-windows.cfg	(revision 7760)
@@ -0,0 +1,114 @@
+[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
+extends= http://grok.zope.org/releaseinfo/grok-1.1.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.
+
+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 = 1.5.0
+zc.recipe.egg = 1.3.0
+zc.recipe.testrunner = 1.4.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 =
+ZODB3 = 3.10.3
+
+[app]
+# This creates only the paster script in bin/.
+recipe = zc.recipe.egg
+eggs = waeup.sirp
+       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.sirp/branches/ulif-schoolgrades/buildout.cfg
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/buildout.cfg	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/buildout.cfg	(revision 7760)
@@ -0,0 +1,189 @@
+[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
+# sirpctl creation must be _after_ app!
+    sirpctl
+# 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
+extends= http://grok.zope.org/releaseinfo/grok-1.1.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.
+
+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 = 1.5.0
+zc.recipe.egg = 1.3.0
+zc.recipe.testrunner = 1.4.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
+
+[app]
+# This creates all scripts in bin/. The sirpctl created here is 'faulty'.
+recipe = zc.recipe.egg
+eggs = waeup.sirp [beaker]
+       z3c.evalexception>=2.0
+       Paste
+       PasteScript
+       PasteDeploy
+       repoze.profile
+interpreter = python-console
+
+[sirpctl]
+# See http://pypi.python.org/pypi/zc.recipe.egg for details...
+# Here we create the sirpctl script again, this time with a
+# fixed path to zdaemon.conf as argument.
+recipe = zc.recipe.egg
+eggs = waeup.sirp [beaker]
+arguments = "${buildout:parts-directory}/etc/zdaemon.conf"
+scripts = sirpctl=sirpctl
+
+[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.sirp [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.sirp
+eggs = waeup.sirp
+domain = waeup.sirp
+output = src/waeup/sirp/locales
+zcml =
+
+# This section is named so that the zpasswd utility is
+# called `zpasswd`
+[zpasswd]
+recipe = z3c.recipe.dev:script
+eggs = waeup.sirp
+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.sirp[docs]
+source = ${buildout:directory}/docs/source
+build = ${buildout:directory}/docs/build
+
+# Collect test coverage data.
+[coverage-detect]
+recipe = zc.recipe.testrunner
+eggs = waeup.sirp[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.sirp/branches/ulif-schoolgrades/docs/INSTALL.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/INSTALL.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/INSTALL.txt	(revision 7760)
@@ -0,0 +1,84 @@
+Installation on Linux production system
+***************************************
+
+.. note:: `waeup.sirp` might not work with Python > 2.7
+          currently. Use of Python 2.6 or 2.7 is recommended.
+
+Prerequisites
+=============
+
+The `waeup.sirp` 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.sirp` 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.sirp` application
+======================================
+
+You can start Zope with the with the following command::
+
+    $ bin/sirpctl 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.sirp/branches/ulif-schoolgrades/docs/INSTALLWIN.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/INSTALLWIN.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/INSTALLWIN.txt	(revision 7760)
@@ -0,0 +1,87 @@
+Installation on Windows (XP, 2000, Vista, 7)
+********************************************
+
+.. note:: `waeup.sirp` 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 SIRP 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 winsirp
+
+**Step 6**. Activate the environment::
+
+ C:\> cd winsirp
+ C:\winsirp> Scripts\activate
+
+Note that the system prompt has changed.
+
+Run SIRP
+========
+
+**Step 7**. Copy the ``waeup.sirp`` folder into ``C:\winsirp``.
+
+**Step 8**. Run the bootstrap script::
+
+ (winsirp) C:\winsirp\waeup.sirp> cd waeup.sirp
+ (winsirp) C:\winsirp\waeup.sirp> python bootstrap\bootstrap.py -c buildout-windows.cfg
+
+**Step 9**. Run buildout (this takes a while)::
+
+ C:\winsirp\waeup.sirp> bin\buildout.exe -c buildout-windows.cfg
+
+**Step 10**. You can deactivate your environment again::
+
+ (winsirp) C:\winsirp\waeup.sirp> ..\Scripts\deactivate
+
+**Step 11**. Start your SIRP::
+
+ C:\winsirp\waeup.sirp> bin\paster serve parts\etc\deploy.ini
+
+or from anywhere in your system::
+
+ C:\[any_path]> C:\winsirp\trunk\bin\paster serve C:\winsirp\trunk\parts\etc\deploy.ini
+
+**Step 12**. Visit the SIRP website and create your university instance:
+
+| http://localhost:8080
+| user name: grok
+| password: grok
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/Makefile
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/Makefile	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/Makefile	(revision 7760)
@@ -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/WAeUPStudentInformationandRegistrationPortalSIRP.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/WAeUPStudentInformationandRegistrationPortalSIRP.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.sirp/branches/ulif-schoolgrades/docs/make.bat
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/make.bat	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/make.bat	(revision 7760)
@@ -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\WAeUPStudentInformationandRegistrationPortalSIRP.qhcp
+	echo.To view the help file:
+	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\WAeUPStudentInformationandRegistrationPortalSIRP.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.sirp/branches/ulif-schoolgrades/docs/source/_templates/mainpage.template
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/_templates/mainpage.template	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/_templates/mainpage.template	(revision 7760)
@@ -0,0 +1,94 @@
+{% extends "layout.html" %}
+{% set title = 'Overview' %}
+{% block body %}
+
+<h1>{{ docstitle }}</h1>
+<p>
+  This is official documentation for waeup.sirp {{ 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.sirp.</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.sirp.
+    </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.sirp/branches/ulif-schoolgrades/docs/source/bugs.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/bugs.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/bugs.rst	(revision 7760)
@@ -0,0 +1,18 @@
+.. _reporting-bugs:
+
+Reporting Bugs in `waeup.sirp`
+******************************
+
+The developers would like to know of any deficiencies you find in
+`waeup.sirp`.
+
+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.sirp/branches/ulif-schoolgrades/docs/source/changes.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/changes.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/changes.rst	(revision 7760)
@@ -0,0 +1,1 @@
+.. include:: ../../CHANGES.txt
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/conf.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/conf.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/conf.py	(revision 7760)
@@ -0,0 +1,213 @@
+# -*- coding: utf-8 -*-
+#
+# WAeUP Student Information and Registration Portal (SIRP) 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.SIRP'
+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.sirp').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 = 'WAeUPStudentInformationandRegistrationPortalSIRPdoc'
+
+
+# -- 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', 'WAeUPStudentInformationandRegistrationPortalSIRP.tex', u'WAeUP Student Information and Registration Portal (SIRP) 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.sirp/branches/ulif-schoolgrades/docs/source/contents.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/contents.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/contents.rst	(revision 7760)
@@ -0,0 +1,25 @@
+.. _contents:
+
+WAeUP.SIRP 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.sirp/branches/ulif-schoolgrades/docs/source/copyright.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/copyright.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/copyright.rst	(revision 7760)
@@ -0,0 +1,10 @@
+Copyright
+*********
+
+`waeup.sirp` 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.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_accesscode.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_accesscode.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_accesscode.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`accesscodes.accesscode` -- AC Components
+*****************************************
+
+.. automodule:: waeup.sirp.accesscodes.accesscode
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_browser.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_browser.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_browser.rst	(revision 7760)
@@ -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.sirp.accesscodes.browser
+     :members:
+     :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_catalog.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_catalog.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_catalog.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`accesscodes.catalog` -- AC Catalog Components
+**********************************************
+
+.. automodule:: waeup.sirp.accesscodes.catalog
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_interfaces.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_interfaces.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_interfaces.rst	(revision 7760)
@@ -0,0 +1,14 @@
+`accesscodes.interfaces` -- AC Interfaces
+*****************************************
+
+.. automodule:: waeup.sirp.accesscodes.interfaces
+   :members:
+   :undoc-members:
+
+
+   .. autointerface:: IAccessCode
+      :members:
+   .. autointerface:: IAccessCodeBatch
+      :members:
+   .. autointerface:: IAccessCodeBatchContainer
+      :members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_workflow.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_workflow.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/accesscodes/api_workflow.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`accesscodes.workflow` - AC Workflow Components
+***********************************************
+
+.. automodule:: waeup.sirp.accesscodes.workflow
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_accesscodes.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_accesscodes.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_accesscodes.rst	(revision 7760)
@@ -0,0 +1,24 @@
+`accesscodes` -- Access Codes Package
+*************************************
+
+(see also: :ref:`api-accesscodes-modules`)
+
+.. automodule:: waeup.sirp.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.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_app.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_app.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_app.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`app` -- University Root (Site) Module
+**************************************
+
+.. automodule:: waeup.sirp.app
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_applicants.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_applicants.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_applicants.rst	(revision 7760)
@@ -0,0 +1,30 @@
+`applicants` - Application Components Package
+*********************************************
+
+(see also: :ref:`api-applicants-modules`)
+
+.. automodule:: waeup.sirp.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_vocabularies
+   applicants/api_workflow
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_authentication.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_authentication.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_authentication.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`authentication` -- Authentication Components Module
+****************************************************
+
+.. automodule:: waeup.sirp.authentication
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_browser.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_browser.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_browser.rst	(revision 7760)
@@ -0,0 +1,27 @@
+`browser` -- Viewing Components Package
+***************************************
+
+(see also: :ref:`api-browser-modules`)
+
+.. automodule:: waeup.sirp.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.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_catalog.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_catalog.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_catalog.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`catalog` -- Cataloging Components Module
+*****************************************
+
+.. automodule:: waeup.sirp.catalog
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_datacenter.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_datacenter.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_datacenter.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`datacenter` -- Datacenter Module
+*********************************
+
+.. automodule:: waeup.sirp.datacenter
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_hostels.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_hostels.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_hostels.rst	(revision 7760)
@@ -0,0 +1,26 @@
+`hostels` -- Accommodation Section Components Package
+*****************************************************
+
+(see also: :ref:`api-accommodation-modules`)
+
+.. automodule:: waeup.sirp.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.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_imagestorage.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_imagestorage.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_imagestorage.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`imagestorage` -- Storing images and other files
+************************************************
+
+.. automodule:: waeup.sirp.imagestorage
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_interfaces.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_interfaces.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_interfaces.rst	(revision 7760)
@@ -0,0 +1,10 @@
+`interfaces` - Main Interfaces Module
+*************************************
+
+.. automodule:: waeup.sirp.interfaces
+   :members:
+   :undoc-members:
+
+.. autointerface:: waeup.sirp.interfaces.IUniversity
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_payments.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_payments.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_payments.rst	(revision 7760)
@@ -0,0 +1,23 @@
+`payments` -- Payment Components Package
+****************************************
+
+(see also: :ref:`api-payments-modules`)
+
+.. automodule:: waeup.sirp.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_vocabularies
+   payments/api_interfaces
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_permissions.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_permissions.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_permissions.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`permissions` -- Permissions and Roles Components Module
+********************************************************
+
+.. automodule:: waeup.sirp.permissions
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_schema.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_schema.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_schema.rst	(revision 7760)
@@ -0,0 +1,21 @@
+`schema` -- Schema Fields
+*************************
+
+(see also: :ref:`api-schema-modules`)
+
+.. automodule:: waeup.sirp.utils
+   :members:
+   :undoc-members:
+
+.. _api-schema-modules:
+
+Submodules
+----------
+
+This package bundles the following modules:
+
+.. toctree::
+   :maxdepth: 1
+
+   schema/api_field
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_students.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_students.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_students.rst	(revision 7760)
@@ -0,0 +1,36 @@
+`students` -- Student Section Components Package
+************************************************
+
+(see also: :ref:`api-students-modules`)
+
+.. automodule:: waeup.sirp.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.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_university.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_university.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_university.rst	(revision 7760)
@@ -0,0 +1,30 @@
+`university` -- University Components Package
+*********************************************
+
+(see also: :ref:`api-university-modules`)
+
+.. automodule:: waeup.sirp.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.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_userscontainer.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_userscontainer.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_userscontainer.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`userscontainer` -- Users (Principals) Module
+*********************************************
+
+.. automodule:: waeup.sirp.userscontainer
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_utils.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_utils.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_utils.rst	(revision 7760)
@@ -0,0 +1,24 @@
+`utils` -- Utilities Package
+****************************
+
+(see also: :ref:`api-utils-modules`)
+
+.. automodule:: waeup.sirp.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.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_widgets.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_widgets.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_widgets.rst	(revision 7760)
@@ -0,0 +1,23 @@
+`widgets` -- Widgets
+********************
+
+(see also: :ref:`api-widgets-modules`)
+
+.. automodule:: waeup.sirp.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.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_workflow.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_workflow.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/api_workflow.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`workflow` -- Workflow Components Module
+****************************************
+
+.. automodule:: waeup.sirp.workflow
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_applicant.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_applicant.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_applicant.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`applicants.applicant` - Applicant Components
+*********************************************
+
+.. automodule:: waeup.sirp.applicants.applicant
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_authentication.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_authentication.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_authentication.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`applicants.authentication` - Applicants Authentication Components
+******************************************************************
+
+.. automodule:: waeup.sirp.applicants.authentication
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_batching.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_batching.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_batching.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`applicants.batching` - Batch Importer for Applicants
+*****************************************************
+
+.. automodule:: waeup.sirp.applicants.batching
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_browser.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_browser.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_browser.rst	(revision 7760)
@@ -0,0 +1,7 @@
+`applicants.browser` - Applicants UI Components
+***********************************************
+
+.. automodule:: waeup.sirp.applicants.browser
+   :members:
+
+   .. :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_container.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_container.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_container.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`applicants.container` - Container for Applicants
+*************************************************
+
+.. automodule:: waeup.sirp.applicants.container
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_interfaces.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_interfaces.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_interfaces.rst	(revision 7760)
@@ -0,0 +1,33 @@
+`applicants.interfaces` - Applicants Related Interfaces
+*******************************************************
+
+.. automodule:: waeup.sirp.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.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_permissions.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_permissions.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_permissions.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`applicants.permissions` - Applicants Permission Components
+***********************************************************
+
+.. automodule:: waeup.sirp.applicants.permissions
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_root.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_root.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_root.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`applicants.root` - Root for Applicants Containers
+**************************************************
+
+.. automodule:: waeup.sirp.applicants.root
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_vocabularies.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_vocabularies.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_vocabularies.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`applicants.vocabularies` - Vocabularies and Sources
+****************************************************
+
+.. automodule:: waeup.sirp.applicants.vocabularies
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_workflow.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_workflow.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/applicants/api_workflow.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`applicants.workflow` - Applicants Workflow Components
+******************************************************
+
+.. automodule:: waeup.sirp.applicants.workflow
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_breadcrumbs.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_breadcrumbs.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_breadcrumbs.rst	(revision 7760)
@@ -0,0 +1,7 @@
+`browser.breadcrumbs` - Browser Breadcrumbs
+*******************************************
+
+.. automodule:: waeup.sirp.browser.breadcrumbs
+   :members:
+   :undoc-members:
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_exceptions.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_exceptions.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_exceptions.rst	(revision 7760)
@@ -0,0 +1,7 @@
+`browser.exceptions` - Browser Exceptions
+*****************************************
+
+.. automodule:: waeup.sirp.browser.exceptions
+   :members:
+   :undoc-members:
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_interfaces.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_interfaces.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_interfaces.rst	(revision 7760)
@@ -0,0 +1,18 @@
+`browser.interfaces` - Browser Interfaces
+*****************************************
+
+.. automodule:: waeup.sirp.browser.interfaces
+   :members:
+   :undoc-members:
+
+   .. autointerface:: IBreadcrumb
+      :members:
+
+   .. autointerface:: IBreadcrumbIgnorable
+      :members:
+
+   .. autointerface:: IBreadcrumbContainer
+      :members:
+
+   .. autointerface:: ITheme
+      :members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_layout.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_layout.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_layout.rst	(revision 7760)
@@ -0,0 +1,7 @@
+`browser.layout` - Browser Layout
+*********************************
+
+.. automodule:: waeup.sirp.browser.layout
+   :members:
+   :undoc-members:
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_pages.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_pages.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_pages.rst	(revision 7760)
@@ -0,0 +1,7 @@
+`browser.pages` - Browser Pages (Views)
+***************************************
+
+.. automodule:: waeup.sirp.browser.pages
+   :members:
+   :undoc-members:
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_resources.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_resources.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_resources.rst	(revision 7760)
@@ -0,0 +1,7 @@
+`browser.resources` - Browser Resources
+***************************************
+
+.. automodule:: waeup.sirp.browser.resources
+   :members:
+   :undoc-members:
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_theming.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_theming.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_theming.rst	(revision 7760)
@@ -0,0 +1,7 @@
+`browser.theming` - Browser Theming
+***********************************
+
+.. automodule:: waeup.sirp.browser.theming
+   :members:
+   :undoc-members:
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_viewlets.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_viewlets.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/browser/api_viewlets.rst	(revision 7760)
@@ -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.sirp.browser.viewlets
+     :members:
+     :undoc-members:
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_browser.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_browser.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_browser.rst	(revision 7760)
@@ -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.sirp.hostels.browser
+     :members:
+     :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_catalog.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_catalog.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_catalog.rst	(revision 7760)
@@ -0,0 +1,8 @@
+`hostels.catalog` - Hostel Catalog
+**********************************
+
+.. automodule:: waeup.sirp.hostels.catalog
+   :members:
+   :undoc-members:
+
+   .. class:: BedIndexes
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_container.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_container.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_container.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`hostels.container` - Hostels Container Components
+**************************************************
+
+.. automodule:: waeup.sirp.hostels.container
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_hostel.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_hostel.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_hostel.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`hostels.hostel` - Hostel Components
+************************************
+
+.. automodule:: waeup.sirp.hostels.hostel
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_interfaces.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_interfaces.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_interfaces.rst	(revision 7760)
@@ -0,0 +1,18 @@
+`hostels.interfaces` - Hostel Related Interfaces
+************************************************
+
+.. automodule:: waeup.sirp.hostels.interfaces
+   :members:
+   :undoc-members:
+
+   .. autointerface:: IHostelsContainer
+      :members:
+
+   .. autointerface:: IHostel
+      :members:
+
+   .. autointerface:: IBed
+      :members:
+
+   .. autointerface:: IBedAllocateStudent
+      :members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_permissions.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_permissions.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_permissions.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`hostels.permissions` - Hostel Permissions
+******************************************
+
+.. automodule:: waeup.sirp.hostels.permissions
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_vocabularies.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_vocabularies.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/hostels/api_vocabularies.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`hostels.vocabularies` - Hostel Vocabularies
+********************************************
+
+.. automodule:: waeup.sirp.hostels.vocabularies
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/index.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/index.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/index.rst	(revision 7760)
@@ -0,0 +1,33 @@
+API
+***
+
+.. automodule:: waeup.sirp
+   :members:
+
+Modules and Packages
+--------------------
+
+`waeup.sirp` 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.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/payments/api_container.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/payments/api_container.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/payments/api_container.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`payments.container` - Payments Container Components
+****************************************************
+
+.. automodule:: waeup.sirp.payments.container
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/payments/api_interfaces.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/payments/api_interfaces.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/payments/api_interfaces.rst	(revision 7760)
@@ -0,0 +1,18 @@
+`payments.interfaces` - Payment Interfaces
+******************************************
+
+.. automodule:: waeup.sirp.payments.interfaces
+   :members:
+   :undoc-members:
+
+   .. autointerface:: IPaymentsContainer
+      :members:
+
+   .. autointerface:: IPayment
+      :members:
+
+   .. autointerface:: ISCPayment
+      :members:
+
+   .. autointerface:: IOnlinePayment
+      :members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/payments/api_payment.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/payments/api_payment.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/payments/api_payment.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`payments.payment` - Payment Components
+***************************************
+
+.. automodule:: waeup.sirp.payments.payment
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/payments/api_vocabularies.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/payments/api_vocabularies.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/payments/api_vocabularies.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`payments.vocabularies` - Payment Vocabularies
+**********************************************
+
+.. automodule:: waeup.sirp.payments.vocabularies
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/schema/api_field.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/schema/api_field.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/schema/api_field.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`schema.field` - Special Fields
+*******************************
+
+.. automodule:: waeup.sirp.schema.field
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_accommodation.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_accommodation.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_accommodation.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`students.accommodation` - Student Accommodation Components
+***********************************************************
+
+.. automodule:: waeup.sirp.students.accommodation
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_authentication.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_authentication.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_authentication.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`students.authentication` - Student Authentication Components
+*************************************************************
+
+.. automodule:: waeup.sirp.students.authentication
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_batching.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_batching.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_batching.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`students.batching` - Student Import and Export Components
+**********************************************************
+
+.. automodule:: waeup.sirp.students.batching
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_browser.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_browser.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_browser.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`students.browser` - Students UI Components
+*******************************************
+
+.. automodule:: waeup.sirp.students.browser
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_catalog.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_catalog.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_catalog.rst	(revision 7760)
@@ -0,0 +1,8 @@
+`students.catalog` - Students Catalog Components
+************************************************
+
+.. automodule:: waeup.sirp.students.catalog
+   :members:
+   :undoc-members:
+
+   .. class:: StudentIndexes
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_container.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_container.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_container.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`students.container` - Students Container
+*****************************************
+
+.. automodule:: waeup.sirp.students.container
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_dynamicroles.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_dynamicroles.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_dynamicroles.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`students.dynamicroles` - Dynamic Roles Components
+**************************************************
+
+.. automodule:: waeup.sirp.students.dynamicroles
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_interfaces.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_interfaces.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_interfaces.rst	(revision 7760)
@@ -0,0 +1,60 @@
+`students.interfaces` - Students Related Interfaces
+***************************************************
+
+.. automodule:: waeup.sirp.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.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_payments.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_payments.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_payments.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`students.payments` - Student Payments
+**************************************
+
+.. automodule:: waeup.sirp.students.payments
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_permissions.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_permissions.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_permissions.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`students.permissions` - Student Permissions
+********************************************
+
+.. automodule:: waeup.sirp.students.permissions
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_student.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_student.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_student.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`students.student` - Student Components
+***************************************
+
+.. automodule:: waeup.sirp.students.student
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_studycourse.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_studycourse.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_studycourse.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`students.studycourse` - Student Study Course Components
+********************************************************
+
+.. automodule:: waeup.sirp.students.studycourse
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_studylevel.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_studylevel.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_studylevel.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`students.studylevel` - Student Study Level Components
+******************************************************
+
+.. automodule:: waeup.sirp.students.studylevel
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_utils.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_utils.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_utils.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`students.utils` - Student Utilities
+************************************
+
+.. automodule:: waeup.sirp.students.utils
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_viewlets.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_viewlets.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_viewlets.rst	(revision 7760)
@@ -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.sirp.students.viewlets
+     :members:
+     :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_vocabularies.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_vocabularies.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_vocabularies.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`students.vocabularies` - Student Vocabularies
+**********************************************
+
+.. automodule:: waeup.sirp.students.vocabularies
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_workflow.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_workflow.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/students/api_workflow.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`students.workflow` - Student Workflow
+**************************************
+
+.. automodule:: waeup.sirp.students.workflow
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_batching.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_batching.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_batching.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`university.batching` - Importer Components
+*******************************************
+
+.. automodule:: waeup.sirp.university.batching
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_catalog.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_catalog.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_catalog.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`university.catalog` - Cataloging Components
+********************************************
+
+.. automodule:: waeup.sirp.university.catalog
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_certificate.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_certificate.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_certificate.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`university.certificate` - Certificate Components
+*************************************************
+
+.. automodule:: waeup.sirp.university.certificate
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_certificatescontainer.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_certificatescontainer.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_certificatescontainer.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`university.certificatescontainer` - Certificates Container Components
+**********************************************************************
+
+.. automodule:: waeup.sirp.university.certificatescontainer
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_course.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_course.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_course.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`university.course` - Course Components
+***************************************
+
+.. automodule:: waeup.sirp.university.course
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_coursescontainer.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_coursescontainer.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_coursescontainer.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`university.coursescontainer` - Courses Container Components
+************************************************************
+
+.. automodule:: waeup.sirp.university.coursescontainer
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_department.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_department.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_department.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`university.department` - Department Components
+***********************************************
+
+.. automodule:: waeup.sirp.university.department
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_facultiescontainer.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_facultiescontainer.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_facultiescontainer.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`university.facultiescontainer` - Faculties Container Components
+****************************************************************
+
+.. automodule:: waeup.sirp.university.facultiescontainer
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_faculty.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_faculty.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_faculty.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`university.faculty` - Faculty Components
+*****************************************
+
+.. automodule:: waeup.sirp.university.faculty
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_interfaces.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_interfaces.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_interfaces.rst	(revision 7760)
@@ -0,0 +1,45 @@
+`university.interfaces` - Interfaces of the University Package
+**************************************************************
+
+.. automodule:: waeup.sirp.university.interfaces
+   :members:
+   :undoc-members:
+
+.. autointerface:: waeup.sirp.university.interfaces.IFaculty
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.IFacultyAdd
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.IFacultiesContainer
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.IDepartment
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.IDepartmentAdd
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.ICoursesContainer
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.ICourse
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.ICourseAdd
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.ICertificate
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.ICertificateAdd
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.ICertificatesContainer
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.ICertificateCourse
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.ICertificateCourseAdd
+   :members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_vocabularies.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_vocabularies.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/university/api_vocabularies.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`university.vocabularies` - Vocabularies and Sources
+****************************************************
+
+.. automodule:: waeup.sirp.university.vocabularies
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_batching.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_batching.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_batching.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`utils.batching` - Batching Components
+**************************************
+
+.. automodule:: waeup.sirp.utils.batching
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_converters.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_converters.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_converters.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`utils.converters` - Converter Components
+*****************************************
+
+.. automodule:: waeup.sirp.utils.converters
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_helpers.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_helpers.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_helpers.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`utils.helpers` - Helper Components
+***********************************
+
+.. automodule:: waeup.sirp.utils.helpers
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_logger.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_logger.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_logger.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`utils.logger` - Logger Components
+**********************************
+
+.. automodule:: waeup.sirp.utils.logger
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_utils.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_utils.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/utils/api_utils.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`utils.utils` - Utilities
+*************************
+
+.. automodule:: waeup.sirp.utils.utils
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/widgets/api_datewidget.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/widgets/api_datewidget.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/widgets/api_datewidget.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`widgets.datewidget` - Date Widget
+**********************************
+
+.. automodule:: waeup.sirp.widgets.datewidget
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/widgets/api_objectwidget.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/widgets/api_objectwidget.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/widgets/api_objectwidget.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`widgets.objectwidget` - Object Widget
+**************************************
+
+.. automodule:: waeup.sirp.widgets.objectwidget
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/widgets/api_phonewidget.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/widgets/api_phonewidget.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/widgets/api_phonewidget.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`widgets.phonewidget` - Phone Widget
+************************************
+
+.. automodule:: waeup.sirp.widgets.phonewidget
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/widgets/api_restwidget.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/widgets/api_restwidget.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/api/widgets/api_restwidget.rst	(revision 7760)
@@ -0,0 +1,6 @@
+`widgets.restwidget` - ReST Widget
+**********************************
+
+.. automodule:: waeup.sirp.widgets.restwidget
+   :members:
+   :undoc-members:
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/developernotes.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/developernotes.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/developernotes.rst	(revision 7760)
@@ -0,0 +1,344 @@
+Developer Notes
+***************
+
+How to setup a developer instance of the WAeUP.SIRP, handle tests, docs
+etc.
+
+The new WAeUP.SIRP 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.SIRP 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.SIRP from the subversion server::
+
+  $ cd mysandbox/
+  $ svn co https://svn.waeup.org/repos/main/waeup.sirp/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.sirp` package we try to reach high standards in
+both, documentation and testing.
+
+:mod:`waeup.sirp` 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.sirp` project can easily be
+created doing::
+
+  $ bin/waeupdocs
+
+This will create a tree of HTML pages in
+``parts/waeupdocs/waeup.sirp/build/waeup.sirp/`` 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.sirp`
+package for examples.
+
+
+Testing
+=======
+
+Tests are most important to the reliability of the :mod:`waeup.sirp`
+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.sirp` 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 `WAeUPSIRPUnitTestLayer` defined in `waeup.sirp.testing`. This
+  layer groks the whole `waeup.sirp` 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.sirp.testing.WAeUPSIRPUnitTestLayer
+
+  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.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/index.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/index.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/index.rst	(revision 7760)
@@ -0,0 +1,9 @@
+Developer Documentation
+***********************
+
+.. toctree::
+   :maxdepth: 2
+   
+   developernotes
+   api/index
+   profiling
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/profiling.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/profiling.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/developerdocs/profiling.rst	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/docs/source/documentation.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/documentation.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/documentation.rst	(revision 7760)
@@ -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.sirp` itself.
+
+.. _Sphinx: http://sphinx.org/
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/glossary.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/glossary.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/glossary.rst	(revision 7760)
@@ -0,0 +1,16 @@
+.. _glossary:
+
+Glossary
+********
+
+.. glossary::
+
+   SIRP
+     Student Information and Registration Portal.
+
+   WAeUP
+     West African e-University Project.
+
+   waeup.sirp
+     A Python package explained in this documentation.
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/index.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/index.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/index.rst	(revision 7760)
@@ -0,0 +1,11 @@
+.. WAeUP Student Information and Registration Portal (SIRP) 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.sirp/branches/ulif-schoolgrades/docs/source/intro.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/intro.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/intro.rst	(revision 7760)
@@ -0,0 +1,1 @@
+.. include:: ../../README.txt
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/license.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/license.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/license.rst	(revision 7760)
@@ -0,0 +1,11 @@
+.. _license:
+
+***********
+The License
+***********
+
+`waeup.sirp` 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.sirp/branches/ulif-schoolgrades/docs/source/userdocs/buildingdocs.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/userdocs/buildingdocs.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/userdocs/buildingdocs.rst	(revision 7760)
@@ -0,0 +1,61 @@
+.. _building_documentation:
+
+Building Documentation (Linux OS only)
+**************************************
+
+To build the documentation for :mod:`waeup.sirp`, please follow these steps:
+
+Prerequisites
+=============
+
+We assume you already installed `waeup.sirp` 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.sirp` 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.sirp/branches/ulif-schoolgrades/docs/source/userdocs/index.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/userdocs/index.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/userdocs/index.rst	(revision 7760)
@@ -0,0 +1,9 @@
+User Documentation
+******************
+
+.. toctree::
+   :maxdepth: 2
+
+   install
+   install_on_windows
+   buildingdocs
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/userdocs/install.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/userdocs/install.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/userdocs/install.rst	(revision 7760)
@@ -0,0 +1,3 @@
+.. _installing_linux:
+
+.. include:: ../../INSTALL.txt
Index: main/waeup.sirp/branches/ulif-schoolgrades/docs/source/userdocs/install_on_windows.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/docs/source/userdocs/install_on_windows.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/docs/source/userdocs/install_on_windows.rst	(revision 7760)
@@ -0,0 +1,3 @@
+.. _installing_win:
+
+.. include:: ../../INSTALLWIN.txt
Index: main/waeup.sirp/branches/ulif-schoolgrades/etc/README.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/etc/README.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/etc/README.txt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/etc/debug.ini.in
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/etc/debug.ini.in	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/etc/debug.ini.in	(revision 7760)
@@ -0,0 +1,67 @@
+# debug.ini
+#
+# Debugging configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi
+
+[handlers]
+keys = console, accesslog
+
+[formatters]
+keys = generic, accesslog
+
+[formatter_generic]
+format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+
+[formatter_accesslog]
+format = %(message)s
+
+[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
+
+[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:waeup.sirp#debug
+filter-with = translogger
+exempt-exceptions = zope.security.interfaces.IUnauthorized
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 8080
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
Index: main/waeup.sirp/branches/ulif-schoolgrades/etc/deploy.ini.in
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/etc/deploy.ini.in	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/etc/deploy.ini.in	(revision 7760)
@@ -0,0 +1,60 @@
+# deploy.ini
+#
+# Deployment configuration for use with paster/WSGI
+#
+
+[loggers]
+keys = root, wsgi
+
+[handlers]
+keys = console, accesslog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+handlers = accesslog
+qualname = wsgi
+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
+
+[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:waeup.sirp
+filter-with = translogger
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 8080
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
Index: main/waeup.sirp/branches/ulif-schoolgrades/etc/profile.ini.in
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/etc/profile.ini.in	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/etc/profile.ini.in	(revision 7760)
@@ -0,0 +1,66 @@
+# profile.ini
+#
+# Profiling configuration for use with paster/WSGI
+#
+# Call http://localhost:8080/__profile__ to see the profiler in action
+
+[loggers]
+keys = root, wsgi
+
+[handlers]
+keys = console, accesslog
+
+[formatters]
+keys = generic, accesslog
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_wsgi]
+level = INFO
+handlers = accesslog
+qualname = wsgi
+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
+
+[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:waeup.sirp
+
+[pipeline:main]
+pipeline = translogger profiler app
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 8080
+
+[DEFAULT]
+# set the name of the zope.conf file
+zope_conf = %(here)s/zope.conf
Index: main/waeup.sirp/branches/ulif-schoolgrades/etc/site.zcml.in
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/etc/site.zcml.in	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/etc/site.zcml.in	(revision 7760)
@@ -0,0 +1,50 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:sirp="http://namespaces.waeup.org/sirp"
+           i18n_domain="waeup.sirp">
+
+  <include package="waeup.sirp" />
+  <include package="waeup.sirp" file="mail.zcml" />
+
+  <!-- Where should the datacenter reside by default? -->
+  <sirp:datacenter
+      path="${buildout:directory}/var/datacenter" />
+
+    <configure i18n_domain="waeup.sirp">
+
+
+      <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"
+                 />
+
+      <!-- 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.sirp/branches/ulif-schoolgrades/etc/zdaemon.conf.in
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/etc/zdaemon.conf.in	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/etc/zdaemon.conf.in	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/etc/zope.conf.in
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/etc/zope.conf.in	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/etc/zope.conf.in	(revision 7760)
@@ -0,0 +1,55 @@
+# 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>
+
+<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.sirp/branches/ulif-schoolgrades/setup.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/setup.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/setup.py	(revision 7760)
@@ -0,0 +1,126 @@
+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',
+    'PIL',
+    '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',
+    ],
+
+# 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', 'sirp', 'app.txt')
+    + '\n\n'
+    + read('CHANGES.txt')
+    + '\n\n'
+    + 'Download\n'
+    + '********\n'
+    )
+
+setup(name = 'waeup.sirp',
+      version = version,
+      description = "A student online information and  registration portal",
+      long_description = long_description,
+
+      keywords = "portal waeup sirp student university registration grok zope",
+      # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      classifiers = [
+        'Development Status :: 3 - Alpha',
+        '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_sirp = waeup.sirp.browser.resources:waeup_sirp
+      [console_scripts]
+      sirp-debug = grokcore.startup:interactive_debug_prompt
+      sirpctl = grokcore.startup:zdaemon_controller
+      [paste.app_factory]
+      main = grokcore.startup:application_factory
+      debug = grokcore.startup:debug_application_factory
+
+      """,
+      )
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/__init__.py	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/__init__.py	(revision 7760)
@@ -0,0 +1,13 @@
+import grok
+from waeup.sirp.interfaces import ISIRPObject
+from zope.annotation.attribute import AttributeAnnotations
+from zope.annotation.interfaces import IAnnotations
+
+class SIRPAttributeAnnotations(AttributeAnnotations, grok.Adapter):
+    """An adapter to IAnnotations for any SIRPObject.
+
+    Providing this adapter, each SIRP object becomes (attribute)
+    annotatable.
+    """
+    grok.provides(IAnnotations)
+    grok.context(ISIRPObject)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/__init__.py	(revision 7760)
@@ -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.sirp.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.sirp.accesscodes.browser`.
+
+The interfaces of this subpackage are defined in
+:mod:`waeup.sirp.accesscodes.interfaces`.
+"""
+from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/accesscode.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/accesscode.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/accesscode.py	(revision 7760)
@@ -0,0 +1,595 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 csv
+import grok
+import os
+from datetime import datetime
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from random import SystemRandom as random
+from waeup.sirp.interfaces import ISIRPPluggable, IObjectHistory
+from waeup.sirp.utils.logger import Logger
+from waeup.sirp.accesscodes.interfaces import (
+    IAccessCode, IAccessCodeBatch, IAccessCodeBatchContainer
+    )
+from waeup.sirp.accesscodes.workflow import DISABLED, USED, ac_states_dict
+
+class AccessCode(grok.Model):
+    """An access code (aka PIN).
+
+    Implements
+    :class:`waeup.sirp.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.sirp.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.sirp.accesscodes.workflow` for details.
+
+    """
+    grok.implements(IAccessCode)
+
+    def __init__(self, batch_serial, random_num):
+        super(AccessCode, self).__init__()
+        self.batch_serial = batch_serial
+        self.random_num = random_num
+        self.owner = 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.sirp.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, creator, batch_prefix, cost,
+                 entry_num, num=None):
+        super(AccessCodeBatch, self).__init__()
+        self.creation_date = creation_date
+        self.creator = creator
+        self.prefix = batch_prefix.upper()
+        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)
+        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, owner=None):
+        """Add an access-code.
+        """
+        ac = AccessCode(num, pin)
+        if owner:
+            ac.owner = owner
+        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()
+        now = datetime.now()
+        timestamp = 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'])
+        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
+                    ])
+        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.sirp.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
+
+    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 and hostel application batches start with 0.
+        # These batches are being emptily created during initialization of the
+        # university instance.
+        if prefix in ('CLR', 'SFE', 'HOS'):
+            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.now(), creator, prefix, cost, 0, num=num)
+        num_entries = 0
+        self[batch_name] = batch
+        for row in reader:
+            pin = row['ac']
+            serial = int(row['serial'])
+            rand_num = pin.rsplit('-', 1)[-1]
+            batch.addAccessCode(serial, rand_num)
+            num_entries += 1
+        batch.entry_num = num_entries
+
+        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.sirp.${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(ISIRPPluggable)
+
+    def setup(self, site, name, logger):
+        basecontainer = AccessCodeBatchContainer()
+        site['accesscodes'] = basecontainer
+        logger.info('Installed container for access code batches.')
+        # Create empty school fee, clearance and hostel application AC
+        # batches during initialization of university instance.
+        cost = 0.0
+        creator = 'system'
+        entry_num = 0
+        creation_date = datetime.now()
+        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)
+        logger.info('Installed empty SFE, CLR and HOS access code batches.')
+        return
+
+    def update(self, site, name, logger):
+        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, ))
+        return
+
+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.sirp.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, 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, owner)
+    batch.entry_num += 1
+    pin = u'%s-%s-%s' % (batch_prefix,batch_num,rand_num)
+    return pin, None
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/accesscode.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/accesscode.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/accesscode.txt	(revision 7760)
@@ -0,0 +1,674 @@
+:mod:`waeup.sirp.accesscodes.accesscode` -- access codes (aka PINs)
+*******************************************************************
+
+.. module:: waeup.sirp.accesscodes.accesscode
+
+Components that represent access codes and related.
+
+.. :NOdoctest:
+.. :NOlayer: waeup.sirp.testing.SIRPUnitTestLayer
+
+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.sirp` 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.sirp.accesscodes.interfaces.IAccessCode`:
+
+    >>> from waeup.sirp.accesscodes.interfaces import IAccessCode
+    >>> from waeup.sirp.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.sirp.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.sirp.accesscodes.interfaces import IAccessCodeBatch
+    >>> from waeup.sirp.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.sirp.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.sirp...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.sirp...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.sirp...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.sirp...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.sirp.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.sirp...AccessCode object at 0x...>,
+     <waeup.sirp...AccessCode object at 0x...>]
+
+    >>> container.search('not-a-number', 'serial')
+    []
+
+    >>> result = container.search('1', 'serial')
+    >>> result
+    [<waeup.sirp...AccessCode object at 0x...>,
+     <waeup.sirp...AccessCode object at 0x...>]
+
+Searching for ACs:
+
+    >>> ac = result[0]
+    >>> container.search(ac.representation, 'pin')
+    [<waeup.sirp...AccessCode object at 0x...>]
+
+Searching for student IDs:
+
+#    >>> ac.__parent__.invalidate(
+#    ...   ac.representation, 'some_user')
+#    >>> container.search('some_user', 'stud_id')
+#    [<waeup.sirp...AccessCode object at 0x...>]
+
+
+Access code plugin
+==================
+
+.. class:: AccessCodePlugin
+
+  A `waeup.sirp` plugin that updates existing SIRP university
+  instances so that they provide support for access-codes.
+
+  .. attribute:: grok.implements(ISIRPPluggable)
+  .. 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
+  ISIRPPluggable 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.sirp.interfaces import ISIRPPluggable
+    >>> plugin = getUtility(ISIRPPluggable, name='accesscodes')
+    >>> plugin
+    <waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/api.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/api.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/api.txt	(revision 7760)
@@ -0,0 +1,17 @@
+:mod:`waeup.sirp.accesscodes` -- Access Codes
+*********************************************
+
+.. module:: waeup.sirp.accesscodes
+
+Components that represent access codes and related components.
+
+Submodules
+==========
+
+The :mod:`waeup.sirp.accesscodes` module provides the following submodules:
+
+.. toctree::
+   :maxdepth: 1
+
+   browser.txt
+   accesscodes.txt
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser.py	(revision 7760)
@@ -0,0 +1,261 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 hurry.workflow.interfaces import InvalidTransitionError
+from waeup.sirp.browser.resources import datatable
+from waeup.sirp.browser import SIRPPage, SIRPAddFormPage, NullValidator
+from waeup.sirp.browser.breadcrumbs import Breadcrumb
+from waeup.sirp.browser.viewlets import (
+    AdminTask, AddActionButton, SearchActionButton, BatchOpButton, ManageLink)
+from waeup.sirp.interfaces import ISIRPObject
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.accesscodes.interfaces import (
+    IAccessCodeBatchContainer, IAccessCodeBatch,
+    )
+from waeup.sirp.accesscodes.catalog import search
+from waeup.sirp.browser.layout import action
+
+grok.context(ISIRPObject)
+
+class BatchContainerPage(SIRPPage):
+    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.sirp.','')
+        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.flash(_('Deleted batch ${a}', mapping = {'a':name}))
+            message = 'deleted: %s' % name
+            self.context.logger_info(ob_class, message)
+
+class AddBatchPage(SIRPAddFormPage):
+    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.now()
+        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.sirp.','')
+        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(SIRPPage):
+    """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.sirp.','')
+            message = 'reimported: %s' % filename
+            self.context.logger_info(ob_class, message)
+        self.redirect(self.url(self.context))
+
+class BatchContainerSearchPage(SIRPPage):
+    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.sirp.','')
+        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(6)
+    grok.require('waeup.manageACBatches')
+
+    link = u'accesscodes'
+    text = _(u'Access Codes')
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser.txt	(revision 7760)
@@ -0,0 +1,448 @@
+:mod:`waeup.sirp.accesscodes.browser` -- UI components for access-codes
+***********************************************************************
+
+.. module:: waeup.sirp.accesscodes.browser
+
+Here we visit the access-code related parts of a SIRP 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.sirp.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>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>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"
+    "BLA","19.12","1","3"
+    "BLA","0","BLA-1-<10-DIGITS>","initialized","..."
+    "BLA","1","BLA-1-<10-DIGITS>","initialized","..."
+    "BLA","2","BLA-1-<10-DIGITS>","initialized","..."
+
+Clean up:
+
+    >>> shutil.rmtree(uploadpath)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser_templates/admintaskacbatches.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser_templates/admintaskacbatches.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser_templates/admintaskacbatches.pt	(revision 7760)
@@ -0,0 +1,5 @@
+<p>
+<a href=""
+   tal:attributes="href viewlet/link_target"
+   tal:content="viewlet/link_title">MANAGE</a>
+</p>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser_templates/batchcontainer.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser_templates/batchcontainer.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser_templates/batchcontainer.pt	(revision 7760)
@@ -0,0 +1,47 @@
+<form i18n:domain="waeup.sirp" 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/(invalidated)</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__" />
+  </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>
+    /
+    <span tal:replace="batch/used_num">512</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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt	(revision 7760)
@@ -0,0 +1,31 @@
+<p i18n:domain="waeup.sirp">
+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.sirp">
+  <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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser_templates/searchpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser_templates/searchpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/browser_templates/searchpage.pt	(revision 7760)
@@ -0,0 +1,52 @@
+<form method="POST" i18n:domain="waeup.sirp">
+<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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/catalog.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/catalog.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/catalog.py	(revision 7760)
@@ -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.sirp.interfaces import IUniversity, IQueryResultItem
+from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/interfaces.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/interfaces.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/interfaces.py	(revision 7760)
@@ -0,0 +1,100 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Interfaces of access code related components.
+"""
+from zope import schema
+from zope.interface import Interface
+from waeup.sirp.interfaces import ISIRPObject
+from waeup.sirp.interfaces import MessageFactory as _
+
+class IAccessCode(ISIRPObject):
+    """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'),
+        default = 0.0, min = 0.0,
+        )
+    state = schema.TextLine(
+        title = _(u'Workflow state'),
+        )
+    representation = schema.TextLine(
+        title = _(u'Complete title of access code'),
+        )
+    owner = schema.TextLine(
+        title = _(u'Purchaser'),
+        )
+    history = schema.Text(
+        title = _(u'The history of access code as lines'),
+        default = u'',
+        readonly = True,
+       )
+
+class IAccessCodeBatch(Interface):
+    """A factory for batches of access codes.
+    """
+    creation_date = schema.Date(
+        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(ISIRPObject):
+    """A container for access code batches.
+    """
+
+    def addBatch(batch):
+        """Add a batch.
+        """
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/__init__.py	(revision 7760)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/sample_import.csv
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/sample_import.csv	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/sample_import.csv	(revision 7760)
@@ -0,0 +1,5 @@
+"prefix","serial","ac","state","history"
+"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"
+"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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/test_accesscode.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/test_accesscode.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/test_accesscode.py	(revision 7760)
@@ -0,0 +1,466 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.hooks import setSite, clearSite
+from zope.interface.verify import verifyObject, verifyClass
+from zope.testing import renormalizing
+from waeup.sirp.app import University
+from waeup.sirp.interfaces import IObjectHistory, ISIRPPluggable
+from waeup.sirp.testing import (
+    FunctionalLayer, FunctionalTestCase, setUp, tearDown, getRootFolder)
+from waeup.sirp.accesscodes.accesscode import (
+    AccessCodeBatch, get_access_code, invalidate_accesscode, AccessCode,
+    disable_accesscode, reenable_accesscode, fire_transition,
+    AccessCodeBatchContainer, AccessCodePlugin)
+from waeup.sirp.accesscodes.interfaces import (
+    IAccessCode, IAccessCodeBatch,  IAccessCodeBatchContainer,)
+from waeup.sirp.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):
+        # We get the cost set in batch
+        cost = self.ac1.cost
+        assert cost == 6.6
+
+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.now(), 'testuser', 'FOO', 9.99, 0)
+        self.app['accesscodes'].addBatch(batch)
+
+        self.ac1 = AccessCode(0, '11111111')
+        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"
+"FOO","9.99","1","0"
+"FOO","0","FOO-1-11111111","used","<YYYY-MM-DD hh:mm:ss> - ..."
+"FOO","1","FOO-1-22222222","initialized","<YYYY-MM-DD hh:mm:ss> - ..."
+"FOO","2","FOO-1-33333333","disabled","<YYYY-MM-DD hh:mm:ss> - ..."
+'''[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_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'])
+
+    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(ISIRPPluggable, plugin)
+        assert verifyClass(ISIRPPluggable, 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/test_catalog.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/test_catalog.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/test_catalog.py	(revision 7760)
@@ -0,0 +1,229 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+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.sirp.app import University
+from waeup.sirp.interfaces import IQueryResultItem
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.accesscodes.accesscode import (
+    AccessCodeBatch, invalidate_accesscode, disable_accesscode)
+from waeup.sirp.accesscodes.workflow import INITIALIZED, USED, DISABLED
+from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/test_workflow.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/test_workflow.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/tests/test_workflow.py	(revision 7760)
@@ -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.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/workflow.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/workflow.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/accesscodes/workflow.py	(revision 7760)
@@ -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.sirp.accesscodes.interfaces import IAccessCode
+from waeup.sirp.interfaces import IObjectHistory, ISIRPWorkflowInfo
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.workflow import SIRPWorkflow, SIRPWorkflowInfo
+
+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 = SIRPWorkflow(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(SIRPWorkflowInfo, grok.Adapter):
+    grok.context(IAccessCode)
+    grok.provides(ISIRPWorkflowInfo)
+
+    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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/app.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/app.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/app.py	(revision 7760)
@@ -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
+##
+import grok
+from zope.authentication.interfaces import IAuthentication
+from zope.component import getUtilitiesFor
+from zope.component.interfaces import ObjectEvent
+from zope.pluggableauth import PluggableAuthentication
+from waeup.sirp.authentication import setup_authentication
+from waeup.sirp.datacenter import DataCenter
+from waeup.sirp.students.container import StudentsContainer
+from waeup.sirp.hostels.container import HostelsContainer
+from waeup.sirp.interfaces import (
+    IUniversity, ISIRPPluggable, IObjectUpgradeEvent, )
+from waeup.sirp.userscontainer import UsersContainer
+from waeup.sirp.utils.logger import Logger
+from waeup.sirp.utils.helpers import attrs_to_fields
+from waeup.sirp.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.
+        """
+        self['users'] = UsersContainer()
+        self['datacenter'] = DataCenter()
+        self['students'] = StudentsContainer()
+        self['configuration'] = ConfigurationContainer()
+        self['hostels'] = HostelsContainer()
+        self._createPlugins()
+
+    def _createPlugins(self):
+        """Create instances of all plugins defined somewhere.
+        """
+        for name, plugin in getUtilitiesFor(ISIRPPluggable):
+            plugin.setup(self, name, self.logger)
+        return
+
+    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(ISIRPPluggable):
+            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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/app.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/app.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/app.txt	(revision 7760)
@@ -0,0 +1,146 @@
+:mod:`waeup.sirp.app` -- central components for SIRP
+****************************************************
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.SIRPUnitTestLayer
+
+.. module:: waeup.sirp.app
+
+.. class:: University
+
+  The main SIRP 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.sirp.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.sirp.app import University
+    >>> myuniversity = University()
+    >>> myuniversity
+    <waeup.sirp.app.University object at 0x...>
+
+Instances of `University` comply with the interface
+`waeup.sirp.interfaces.IUniversity`:
+
+    >>> from zope.interface.verify import verifyClass
+    >>> from waeup.sirp.interfaces import IUniversity
+    >>> verifyClass(IUniversity, University)
+    True
+
+A freshly created instance provides the attributes promised by the
+interface:
+
+    >>> from waeup.sirp.app import University
+    >>> myuniversity = University()
+    >>> myuniversity['configuration'].name
+    u'Sample University'
+
+    >>> myuniversity['faculties']
+    <waeup.sirp.university.facultiescontainer.FacultiesContainer object at 0x...>
+
+  >>> myuniversity['students']
+  <waeup.sirp.students.container.StudentsContainer object at 0x...>
+
+  >>> myuniversity['users']
+  <waeup.sirp.userscontainer.UsersContainer object at 0x...>
+
+  >>> myuniversity['datacenter']
+  <waeup.sirp.datacenter.DataCenter object at 0x...>
+
+  >>> myuniversity['configuration']
+  <waeup.sirp.configuration.ConfigurationContainer object at 0x...>
+
+We can export universities. For this we lookup an appropriate exporter
+first::
+
+  >>> from waeup.sirp.interfaces import ISIRPExporter
+  >>> exporter = ISIRPExporter(myuniversity)
+  >>> exporter
+  <waeup.sirp.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.sirp.interfaces import ISIRPXMLExporter
+  >>> exporter = ISIRPXMLExporter(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>
+
+SIRP plugins
+============
+
+waeup.sirp provides an API to 'plugin' components. Things that should
+be setup at creation time of a SIRP application can indicate
+that by providing a utility providing ISIRPPlugin.
+
+The plugins are looked up by an created app, which then will call the
+``setup()`` method of each plugin.
+
+   >>> from waeup.sirp.interfaces import ISIRPPluggable
+   >>> from zope.component import getAdapters, getUtilitiesFor
+   >>> sorted(list(getUtilitiesFor(ISIRPPluggable)))
+   [(u'accesscodes', <waeup.sirp.accesscodes...AccessCodePlugin ...)]
+
+
+We can provide a new plugin like this:
+
+   >>> import grok
+   >>> from waeup.sirp.interfaces import ISIRPPluggable
+   >>> class MyPlugin(grok.GlobalUtility):
+   ...   grok.implements(ISIRPPluggable)
+   ...   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 SIRP instance, we will get a message:
+
+   >>> from waeup.sirp.app import University
+   >>> site = University()
+   Setup was called for
+   <waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/__init__.py	(revision 7760)
@@ -0,0 +1,21 @@
+"""This package contains everything regarding university applicants.
+"""
+# Make this a package.
+from waeup.sirp.applicants.applicant import (
+    Applicant, ApplicantFactory, ApplicantImageStoreHandler,
+    ApplicantImageNameChooser,
+    )
+from waeup.sirp.applicants.container import ApplicantsContainer
+from waeup.sirp.applicants.root import ApplicantsRoot
+from waeup.sirp.applicants.dynamicroles import (
+    ApplicantPrincipalRoleManager,)
+
+__all__ = [
+    'Applicant',
+    'ApplicantFactory',
+    'ApplicantImageNameChooser',
+    'ApplicantImageStoreHandler',
+    'ApplicantPrincipalRoleManager',
+    'ApplicantsContainer',
+    'ApplicantsRoot',
+    ]
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/applicant.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/applicant.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/applicant.py	(revision 7760)
@@ -0,0 +1,327 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 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 hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from waeup.sirp.app import University
+from waeup.sirp.image import SIRPImageFile
+from waeup.sirp.imagestorage import DefaultFileStoreHandler
+from waeup.sirp.interfaces import (
+    IObjectHistory, IFileStoreHandler, IFileStoreNameChooser, ISIRPUtils,
+    IExtFileStore, IPDF, IUserAccount)
+from waeup.sirp.students.vocabularies import RegNumNotInSource
+from waeup.sirp.utils.helpers import attrs_to_fields
+from waeup.sirp.applicants.interfaces import IApplicant, IApplicantEdit
+from waeup.sirp.applicants.workflow import application_states_dict
+
+class Applicant(grok.Container):
+    grok.implements(IApplicant,IApplicantEdit)
+    grok.provides(IApplicant)
+
+    def __init__(self):
+        super(Applicant, self).__init__()
+        self.password = None
+        IWorkflowInfo(self).fireTransition('init')
+        self.application_date = None
+        self.applicant_id = None
+        return
+
+    def loggerInfo(self, ob_class, comment=None):
+        target = self.applicant_id
+        return grok.getSite()['applicants'].logger_info(ob_class,target,comment)
+
+    @property
+    def state(self):
+        return IWorkflowState(self).getState()
+
+    @property
+    def translated_state(self):
+        return application_states_dict[self.state]
+
+    @property
+    def history(self):
+        history = IObjectHistory(self)
+        return history
+
+    @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)
+        sirp_utils = getUtility(ISIRPUtils)
+        return sirp_utils.fullname(self.firstname, self.lastname, middlename)
+
+    def createStudent(self, view=None):
+        """Create a student, fill with base data, create a StudentApplication
+        object 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.'
+        # Add student object
+        student.firstname = self.firstname
+        student.lastname = self.lastname
+        student.sex = self.sex
+        student.date_of_birth = self.date_of_birth
+        student.email = self.email
+        student.phone = self.phone
+        site = grok.getSite()
+        site['students'].addStudent(student)
+        self.student_id = student.student_id
+        IWorkflowInfo(self).fireTransition('create')
+
+        # Set password
+        IUserAccount(student).setPassword(self.application_number)
+
+        # Save the certificate and set session attributes
+        student['studycourse'].certificate = self.course_admitted
+        student['studycourse'].entry_session = self.__parent__.year
+        student['studycourse'].current_session = self.__parent__.year
+        student['studycourse'].current_level = self.__parent__.entry_level
+        self._copyPassportImage(student)
+        # Update the catalog
+        notify(grok.ObjectModifiedEvent(student))
+
+        # Save application slip
+        self._createApplicationPDF(student, view=view)
+
+        return True, 'Student %s created' % 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 ApplicantCatalog(grok.Indexes):
+    """A catalog indexing :class:`Applicant` instances in the ZODB.
+    """
+    grok.site(University)
+    grok.name('applicants_catalog')
+    grok.context(IApplicant)
+
+    #access_code = index.Field(attribute='access_code')
+    applicant_id = index.Field(attribute='applicant_id')
+    reg_number = index.Field(attribute='reg_number')
+
+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.sirp.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.sirp.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.
+
+        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.
+
+        """
+        parent_name = getattr(
+            getattr(self.context, '__parent__', None),
+            '__name__', '_default')
+        marked_filename = '__%s__%s/%s.jpg' % (
+            APPLICANT_IMAGE_STORE_NAME,
+            parent_name, self.context.applicant_id)
+        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.
+        """
+        ext = os.path.splitext(filename)[1].lower()
+        if ext not in ['.jpg', '.png']:
+            raise ValueError('Only .jpg and .png allowed')
+        # 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, SIRPImageFile(
+            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)
+
+    # 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
+    return
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/authentication.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/authentication.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/authentication.py	(revision 7760)
@@ -0,0 +1,100 @@
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Authenticate applicants.
+"""
+import grok
+from zope.pluggableauth.interfaces import IAuthenticatorPlugin
+from waeup.sirp.interfaces import IAuthPluginUtility, IUserAccount
+from waeup.sirp.applicants.interfaces import IApplicant
+from waeup.sirp.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'
+
+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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/batching.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/batching.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/batching.py	(revision 7760)
@@ -0,0 +1,190 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 csv
+import grok
+from zope.schema import getFields
+from zope.interface import Interface
+from zope.component import queryUtility
+from zope.catalog.interfaces import ICatalog
+from waeup.sirp.interfaces import (
+    IBatchProcessor, IObjectConverter, FatalCSVError)
+from waeup.sirp.utils.batching import BatchProcessor
+from waeup.sirp.applicants.interfaces import (
+    IApplicantsContainer, IApplicant, IApplicantUpdateByRegNo)
+
+class ApplicantsContainerImporter(BatchProcessor):
+    """An importer for applicants containers.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'applicants container importer'
+    grok.name(util_name)
+
+    name = u'Applicants Container Importer'
+    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 ApplicantImporter(BatchProcessor):
+    """A batch processor for IApplicant objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'applicantimporter'
+    grok.name(util_name)
+    name = u'Applicant Importer'
+    iface = IApplicant
+    location_fields = []
+    factory_name = 'waeup.Applicant'
+    location_fields = ['container_code']
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+            ['application_number','reg_number','container_code'] + getFields(
+                self.iface).keys())))
+
+    def checkHeaders(self, headerfields, mode='create'):
+        if not 'reg_number' in headerfields and not 'application_number' \
+            in headerfields:
+            raise FatalCSVError(
+                "Need at least columns application_number or reg_number ")
+        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 getLocator(self, row):
+        #if 'application_number' in row.keys() and row['application_number']:
+        if row.get('application_number',None):
+            return 'application_number'
+        elif 'reg_number' in row.keys() and row['reg_number']:
+            return 'reg_number'
+        else:
+            return None
+
+    def getParent(self, row, site):
+        if not 'applicants' in site.keys():
+            return False
+        return site['applicants'][row['container_code']]
+
+    def parentsExist(self, row, site):
+        return self.getParent(row, site) is not None
+
+    def getEntry(self, row, site):
+        if not self.parentsExist(row, site):
+            return None
+        parent = self.getParent(row, site)
+        if self.getLocator(row) == 'application_number':
+            if row['application_number'] in parent:
+                applicant = parent[row['application_number']]
+                return applicant
+        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:
+                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)
+        return
+
+    def delEntry(self, row, site):
+        applicant = self.getEntry(row, site)
+        if applicant is not None:
+            parent = self.getParent(row, site)
+            del parent[applicant.application_number]
+        pass
+
+    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 ['container_code',
+                'application_number', '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.
+        """
+        if mode in ['update', 'remove']:
+            if self.getLocator(row) == 'reg_number':
+                iface = IApplicantUpdateByRegNo
+        else:
+            iface = self.iface
+        converter = IObjectConverter(iface)
+        errs, inv_errs, conv_dict =  converter.fromStringDict(
+            row, self.factory_name)
+        return errs, inv_errs, conv_dict
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser.py	(revision 7760)
@@ -0,0 +1,953 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 time import time
+from datetime import datetime, date
+from zope.component import getUtility, createObject, getAdapter
+from zope.formlib.form import setUpEditWidgets
+from zope.i18n import translate
+from hurry.workflow.interfaces import (
+    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
+from waeup.sirp.applicants.interfaces import (
+    IApplicant, IApplicantEdit, IApplicantsRoot,
+    IApplicantsContainer, IApplicantsContainerAdd,
+    MAX_UPLOAD_SIZE, IApplicantOnlinePayment, IApplicantsUtils
+    )
+from waeup.sirp.applicants.workflow import INITIALIZED, STARTED, PAID, SUBMITTED
+from waeup.sirp.browser import (
+    SIRPPage, SIRPEditFormPage, SIRPAddFormPage, SIRPDisplayFormPage,
+    DEFAULT_PASSPORT_IMAGE_PATH)
+from waeup.sirp.browser.interfaces import ICaptchaManager
+from waeup.sirp.browser.breadcrumbs import Breadcrumb
+from waeup.sirp.browser.layout import (
+    NullValidator, jsaction, action, UtilityView)
+from waeup.sirp.browser.pages import add_local_role, del_local_roles
+from waeup.sirp.browser.resources import datepicker, tabs, datatable, warning
+from waeup.sirp.interfaces import (
+    ISIRPObject, ILocalRolesAssignable, IExtFileStore, IPDF,
+    IFileStoreNameChooser, IPasswordValidator, IUserAccount, ISIRPUtils)
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.permissions import get_users_with_local_roles
+from waeup.sirp.students.interfaces import IStudentsUtils
+from waeup.sirp.utils.helpers import string_from_bytes, file_size
+from waeup.sirp.widgets.datewidget import (
+    FriendlyDateWidget, FriendlyDateDisplayWidget,
+    FriendlyDatetimeDisplayWidget)
+from waeup.sirp.widgets.phonewidget import PhoneWidget
+from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
+
+grok.context(ISIRPObject) # Make ISIRPObject the default context
+
+class ApplicantsRootPage(SIRPPage):
+    grok.context(IApplicantsRoot)
+    grok.name('index')
+    grok.require('waeup.Public')
+    label = _('Application Section')
+    pnav = 3
+
+    def update(self):
+        super(ApplicantsRootPage, self).update()
+        #datatable.need()
+        return
+
+class ApplicantsRootManageFormPage(SIRPEditFormPage):
+    grok.context(IApplicantsRoot)
+    grok.name('manage')
+    grok.template('applicantsrootmanagepage')
+    label = _('Manage application section')
+    pnav = 3
+    grok.require('waeup.manageApplication')
+    taboneactions = [_('Add applicants container'), _('Remove selected'),
+        _('Cancel')]
+    tabtwoactions1 = [_('Remove selected local roles')]
+    tabtwoactions2 = [_('Add local role')]
+    subunits = _('Applicants Containers')
+
+    def update(self):
+        tabs.need()
+        datatable.need()
+        warning.need()
+        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
+        child_id = form['val_id']
+        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'))
+        return
+
+    @action(_('Add applicants container'), validator=NullValidator)
+    def addApplicantsContainer(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
+
+    @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 ApplicantsContainerAddFormPage(SIRPAddFormPage):
+    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', 'description_dict')
+    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
+    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
+
+    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)
+        appcats_dict = getUtility(IApplicantsUtils).getApplicationTypeDict()
+        title = appcats_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...
+        provider = data['provider'][1]
+        container = provider.factory()
+        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 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 ApplicantsContainerPage(SIRPDisplayFormPage):
+    """The standard view for regular applicant containers.
+    """
+    grok.context(IApplicantsContainer)
+    grok.name('index')
+    grok.require('waeup.Public')
+    grok.template('applicantscontainerpage')
+    pnav = 3
+
+    form_fields = grok.AutoFields(IApplicantsContainer).omit(
+        'title', 'description_dict')
+    form_fields['startdate'].custom_widget = FriendlyDateDisplayWidget('le')
+    form_fields['enddate'].custom_widget = FriendlyDateDisplayWidget('le')
+    form_fields['description'].custom_widget = ReSTDisplayWidget
+
+    @property
+    def introduction(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        lang = self.request.cookies.get('sirp.language', portal_language)
+        html = self.context.description_dict.get(lang,'')
+        if html =='':
+            html = self.context.description_dict.get(portal_language,'')
+        if html =='':
+            return ''
+        else:
+            return html
+
+    @property
+    def label(self):
+        return "%s" % self.context.title
+
+class ApplicantsContainerManageFormPage(SIRPEditFormPage):
+    grok.context(IApplicantsContainer)
+    grok.name('manage')
+    grok.template('applicantscontainermanagepage')
+    form_fields = grok.AutoFields(IApplicantsContainer).omit(
+        'title', 'description_dict')
+    taboneactions = [_('Save'),_('Cancel')]
+    tabtwoactions = [_('Add applicant'), _('Remove selected'),_('Cancel')]
+    tabthreeactions1 = [_('Remove selected local roles')]
+    tabthreeactions2 = [_('Add local role')]
+    # Use friendlier date widget...
+    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
+    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
+    grok.require('waeup.manageApplication')
+
+    @property
+    def label(self):
+        return _('Manage applicants container')
+
+    pnav = 3
+
+    def update(self):
+        datepicker.need() # Enable jQuery datepicker in date fields.
+        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()  # 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):
+        self.applyData(self.context, **data)
+        self.context.description_dict = self._description()
+        self.flash(_('Form has been saved.'))
+        return
+
+    @jsaction(_('Remove selected'))
+    def delApplicant(self, **data):
+        form = self.request.form
+        if form.has_key('val_id'):
+            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(_('Add applicant'), validator=NullValidator)
+    def addApplicant(self, **data):
+        self.redirect(self.url(self.context, 'addapplicant'))
+        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(SIRPAddFormPage):
+    """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(SIRPDisplayFormPage):
+    grok.context(IApplicant)
+    grok.name('index')
+    grok.require('waeup.viewApplication')
+    grok.template('applicantdisplaypage')
+    form_fields = grok.AutoFields(IApplicant).omit(
+        'locked', 'course_admitted', 'password')
+    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
+    label = _('Applicant')
+    pnav = 3
+
+    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} 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', 'firstname', 'lastname','email', 'course1')
+
+class CreateStudentPage(UtilityView, grok.View):
+    """Create a student object from applicatnt data
+    and copy applicant object.
+    """
+    grok.context(IApplicant)
+    grok.name('createstudent')
+    grok.require('waeup.manageStudent')
+
+    def update(self):
+        msg = self.context.createStudent()[1]
+        self.flash(msg)
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class AcceptanceFeePaymentAddPage(UtilityView, grok.View):
+    """ Page to add an online payment ticket
+    """
+    grok.context(IApplicant)
+    grok.name('addafp')
+    grok.require('waeup.payApplicant')
+
+    def update(self):
+        p_category = 'acceptance'
+        session = str(self.context.__parent__.year)
+        try:
+            academic_session = grok.getSite()['configuration'][session]
+        except KeyError:
+            self.flash(_('Session configuration object is not available.'))
+            return
+        timestamp = "%d" % int(time()*1000)
+        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
+        payment = createObject(u'waeup.ApplicantOnlinePayment')
+        payment.p_id = "p%s" % timestamp
+        payment.p_item = self.context.__parent__.title
+        payment.p_year = self.context.__parent__.year
+        payment.p_category = p_category
+        payment.amount_auth = academic_session.acceptance_fee
+        payment.surcharge_1 = academic_session.surcharge_1
+        payment.surcharge_2 = academic_session.surcharge_2
+        payment.surcharge_3 = academic_session.surcharge_3
+        self.context[payment.p_id] = payment
+        self.flash(_('Payment ticket created.'))
+        return
+
+    def render(self):
+        usertype = getattr(self.request.principal, 'user_type', None)
+        if usertype == 'applicant':
+            self.redirect(self.url(self.context, '@@edit'))
+            return
+        self.redirect(self.url(self.context, '@@manage'))
+        return
+
+
+class OnlinePaymentDisplayFormPage(SIRPDisplayFormPage):
+    """ Page to view an online payment ticket
+    """
+    grok.context(IApplicantOnlinePayment)
+    grok.name('index')
+    grok.require('waeup.viewApplication')
+    form_fields = grok.AutoFields(IApplicantOnlinePayment)
+    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 OnlinePaymentCallbackPage(UtilityView, grok.View):
+    """ Callback view
+    """
+    grok.context(IApplicantOnlinePayment)
+    grok.name('callback')
+    grok.require('waeup.payApplicant')
+
+    # This update method simulates a valid callback und must be
+    # specified in the customization package. The parameters must be taken
+    # from the incoming request.
+    def update(self):
+        self.wf_info = IWorkflowInfo(self.context.__parent__)
+        try:
+            self.wf_info.fireTransition('pay')
+        except InvalidTransitionError:
+            self.flash('Error: %s' % sys.exc_info()[1])
+            return
+        self.context.r_amount_approved = self.context.amount_auth
+        self.context.r_card_num = u'0000'
+        self.context.r_code = u'00'
+        self.context.p_state = 'paid'
+        self.context.payment_date = datetime.now()
+        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
+        self.context.__parent__.loggerInfo(
+            ob_class, 'valid callback: %s' % self.context.p_id)
+        self.flash(_('Valid callback received.'))
+        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_receipt.pdf')
+    grok.require('waeup.viewApplication')
+    form_fields = grok.AutoFields(IApplicantOnlinePayment)
+    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
+    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
+    prefix = 'form'
+
+    @property
+    def title(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        return translate(_('Payment Data'), 'waeup.sirp',
+            target_language=portal_language)
+
+    @property
+    def label(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        return translate(_('Online Payment Receipt'),
+            'waeup.sirp', 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_receipt.pdf',
+            self.context.__parent__, applicantview)
+
+class ExportPDFPage(UtilityView, grok.View):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(IApplicant)
+    grok.name('application_slip.pdf')
+    grok.require('waeup.viewApplication')
+    form_fields = grok.AutoFields(IApplicant).omit(
+        'locked', 'course_admitted')
+    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
+    prefix = 'form'
+
+    #@property
+    #def label(self):
+    #    portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+    #    container_title = self.context.__parent__.title
+    #    label = translate('Application Record',
+    #        'waeup.sirp', target_language=portal_language)
+    #    return container_title +  label + self.context.application_number
+
+    #def getCourseAdmitted(self):
+    #    """Return title and code in html format to the certificate
+    #       admitted.
+    #    """
+    #    course_admitted = self.context.course_admitted
+    #    #if ICertificate.providedBy(course_admitted):
+    #    if getattr(course_admitted, '__parent__',None):
+    #        title = course_admitted.title
+    #        code = course_admitted.code
+    #        return '%s - %s' %(code,title)
+    #    return ''
+
+    #def setUpWidgets(self, ignore_request=False):
+    #    self.adapters = {}
+    #    self.widgets = setUpEditWidgets(
+    #        self.form_fields, self.prefix, self.context, self.request,
+    #        adapters=self.adapters, for_display=True,
+    #        ignore_request=ignore_request
+    #        )
+
+    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(SIRPEditFormPage):
+    """A full edit view for applicant data.
+    """
+    grok.context(IApplicant)
+    grok.name('manage')
+    grok.require('waeup.manageApplication')
+    form_fields = grok.AutoFields(IApplicant)
+    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
+    form_fields['student_id'].for_display = True
+    form_fields['applicant_id'].for_display = True
+    form_fields['phone'].custom_widget = PhoneWidget
+    grok.template('applicanteditpage')
+    manage_applications = True
+    pnav = 3
+    display_actions = [[_('Save'), _('Final Submit')],
+        [_('Add online payment ticket'),_('Remove selected tickets')]]
+
+    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.passport_changed = None
+        upload = self.request.form.get('form.passport', None)
+        if upload:
+            # We got a fresh upload
+            self.passport_changed = handle_img_upload(
+                upload, self.context, self)
+        return
+
+    @property
+    def label(self):
+        container_title = self.context.__parent__.title
+        return _('${a} 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 = self.wf_info.getManualTransitions()
+        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.passport_changed is False:  # False is not None!
+            return # error during image upload. Ignore other values
+        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 self.passport_changed:
+            changed_fields.append('passport')
+        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.'))
+        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
+        if fields_string:
+            self.context.loggerInfo(ob_class, '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 form.has_key('val_id'):
+            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)}))
+            ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
+            self.context.loggerInfo(
+                ob_class, '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
+
+class ApplicantEditFormPage(ApplicantManageFormPage):
+    """An applicant-centered edit view for applicant data.
+    """
+    grok.context(IApplicantEdit)
+    grok.name('edit')
+    grok.require('waeup.handleApplication')
+    form_fields = grok.AutoFields(IApplicantEdit).omit(
+        'locked', 'course_admitted', 'student_id',
+        'screening_score', 'reg_number'
+        )
+    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
+    form_fields['phone'].custom_widget = PhoneWidget
+    form_fields['applicant_id'].for_display = True
+    grok.template('applicanteditpage')
+    manage_applications = False
+
+    @property
+    def display_actions(self):
+        state = IWorkflowState(self.context).getState()
+        if state == INITIALIZED:
+            actions = [[],[]]
+        elif state == STARTED:
+            actions = [[_('Save')],
+                [_('Add online payment ticket'),_('Remove selected tickets')]]
+        elif state == PAID:
+            actions = [[_('Save'), _('Final Submit')],
+                [_('Remove selected tickets')]]
+        else:
+            actions = [[],[]]
+        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:
+            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'))
+    def save(self, **data):
+        if self.passport_changed is False:  # False is not None!
+            return # error during image upload. Ignore other values
+        self.applyData(self.context, **data)
+        self.flash('Form has been saved.')
+        return
+
+    @action(_('Final Submit'))
+    def finalsubmit(self, **data):
+        if self.passport_changed 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 != PAID:
+            self.flash(_('This form cannot be submitted. Wrong state!'))
+            return
+        IWorkflowInfo(self.context).fireTransition('submit')
+        self.context.application_date = datetime.now()
+        self.context.locked = True
+        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(SIRPAddFormPage):
+    """Captcha'd registration page for applicants.
+    """
+    grok.context(IApplicantsContainer)
+    grok.name('register')
+    grok.require('waeup.Anonymous')
+    grok.template('applicantregister')
+    form_fields = grok.AutoFields(IApplicantEdit).select(
+        'firstname', 'middlename', 'lastname', 'email', 'phone')
+    form_fields['phone'].custom_widget = PhoneWidget
+
+    @property
+    def label(self):
+        return _('Register for ${a} Application',
+            mapping = {'a':self.context.title})
+
+    def update(self):
+        # Check if application has started ...
+        if not self.context.startdate or self.context.startdate > date.today():
+            self.flash(_('Application has not yet started.'))
+            self.redirect(self.url(self.context))
+            return
+        # ... or ended
+        if not self.context.enddate or self.context.enddate < date.today():
+            self.flash(_('Application has ended.'))
+            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
+
+    @action(_('Get login credentials'), 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
+        # Add applicant and create password
+        applicant = createObject('waeup.Applicant')
+        self.applyData(applicant, **data)
+        self.context.addApplicant(applicant)
+        sirp_utils = getUtility(ISIRPUtils)
+        password = sirp_utils.genPassword()
+        IUserAccount(applicant).setPassword(password)
+        # Send email with credentials
+        login_url = self.url(grok.getSite(), 'login')
+        msg = _('You have successfully been registered for the')
+        if sirp_utils.sendCredentials(IUserAccount(applicant),
+            password, login_url, msg):
+            self.redirect(self.url(self.context, 'registration_complete',
+                                   data = dict(email=applicant.email)))
+            return
+        else:
+            self.flash(_('Email could not been sent. Please retry later.'))
+        return
+
+class ApplicantRegistrationEmailSent(SIRPPage):
+    """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):
+        self.email = email
+        return
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt	(revision 7760)
@@ -0,0 +1,68 @@
+<h2>
+... <span tal:replace="context/translated_state">APPLICATIONSTATE</span> ...
+</h2>
+
+<div class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">MESSAGE</span><br />
+  </div>
+</div>
+
+<img src="" tal:attributes="src view/passport_url" />
+
+<table i18n:domain="waeup.sirp" class="form-table">
+  <tbody>
+    <tal:block repeat="widget view/widgets">
+      <tr>
+        <td class="fieldname">
+          <tal:block content="widget/label"/>:
+        </td>
+        <td>
+          <span tal:replace="structure widget" />
+        </td>
+      </tr>
+    </tal:block>
+    <tr>
+      <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.sirp" i18n:translate="">
+    Acceptance Fee Payment Tickets
+</h3>
+<table i18n:domain="waeup.sirp">
+  <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/p_item">ITEM</td>
+      <td tal:content ="value/state">STATE</td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicanteditpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicanteditpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicanteditpage.pt	(revision 7760)
@@ -0,0 +1,116 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.sirp" enctype="multipart/form-data">
+
+  <h2>
+  ... <span tal:replace="context/translated_state">APPLICATIONSTATE</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="">
+          Photograph
+        </td>
+        <td>
+          <img src="passport.jpg" /><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>
+
+  <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="">
+      Acceptance 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/p_item">ITEM</td>
+      <td tal:content ="cl/state">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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantregemailsent.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantregemailsent.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantregemailsent.pt	(revision 7760)
@@ -0,0 +1,14 @@
+<div i18n:domain="waeup.sirp">
+  <p>
+    <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="">
+    Please check your email account to proceed.
+  </p>
+  <p  i18n:translate="">
+    Thanks for using SIRP!
+  </p>
+</div>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantregister.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantregister.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantregister.pt	(revision 7760)
@@ -0,0 +1,27 @@
+<form action="." tal:attributes="action request/URL" method="post"
+    i18n:domain="waeup.sirp" 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="">
+    An account will be created for you 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantscontaineraddpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantscontaineraddpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantscontaineraddpage.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt	(revision 7760)
@@ -0,0 +1,139 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+  i18n:domain="waeup.sirp" 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">
+      <h3 i18n:translate="">Applicants</h3>
+      <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>
+    </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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantscontainerpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantscontainerpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantscontainerpage.pt	(revision 7760)
@@ -0,0 +1,21 @@
+<span tal:condition="view/introduction" tal:omit-tag="">
+  <span tal:content="structure view/introduction">INTRODUCTION</span>
+  <br />
+</span>
+
+<table class="form-table" tal:condition="layout/isAuthenticated">
+  <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>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt	(revision 7760)
@@ -0,0 +1,106 @@
+<form action="." tal:attributes="action request/URL"
+  i18n:domain="waeup.sirp" method="POST"  enctype="multipart/form-data">
+  <ul class="tabs" data-tabs="tabs">
+    <li class="active">
+    <a href="#tab-1">
+      <span tal:content="view/subunits">Contents
+      </span></a>
+    </li>
+    <li>
+    <a href="#tab-2">
+      <span i18n:translate="">Local Roles
+      </span></a>
+    </li>
+  </ul>
+  <div class="tab-content">
+    <div id="tab-1" class="active">
+      <h3 tal:content="view/subunits">SUBUNITS</h3>
+      <table class="display dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th i18n:translate="">Year
+            </th>
+            <th i18n:translate="">Code
+            </th>
+            <th i18n:translate="">Title
+            </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)"
+                tal:content="entry/__name__">CODE</a> </td>
+            <td tal:content="entry/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.taboneactions"
+            tal:replace="structure action/render"/>
+        </span>
+      </div>
+    </div>
+    <div id="tab-2"> <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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantsrootpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantsrootpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/browser_templates/applicantsrootpage.pt	(revision 7760)
@@ -0,0 +1,28 @@
+<table i18n:domain="waeup.sirp">
+  <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 context/values" 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.formatDate(entry.startdate)">START</span>
+      -
+      <span tal:content="python: layout.formatDate(entry.enddate)">END</span>
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/container.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/container.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/container.py	(revision 7760)
@@ -0,0 +1,120 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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
+from zope.component.factory import Factory
+from zope.component.interfaces import IFactory
+from waeup.sirp.applicants.interfaces import (
+    IApplicantsContainer, IApplicantsContainerAdd,
+    IApplicantsContainerProvider, IApplicant
+    )
+from waeup.sirp.utils.helpers import attrs_to_fields
+
+def generate_applicant_id(container=None):
+    if container is not None:
+        aid = u"%s_%d" % (container.code, r().randint(99999,1000000))
+        while aid in container.keys():
+            aid = u"%s_%d" % (container.code, r().randint(99999,1000000))
+        return aid
+    else:
+        # In some tests we don't use containers
+        return u"xxx_1234"
+
+class ApplicantsContainer(grok.Container):
+    """An applicants container contains university applicants.
+    """
+    grok.implements(IApplicantsContainer,IApplicantsContainerAdd)
+
+    #: These are 'class-attributes'. Do not fiddle around with it in
+    #: instances of this class.
+    #:
+    #: The title of this container as displayed in add-forms. It should
+    #: give some idea about what kind of container this is (one or two words)
+    container_title = u'Basic'
+
+    #: Another 'class-attribute'. Do not fiddle around with it in instances
+    #: of this class.
+    #:
+    #: The `container_description` gives a short explanation about for what
+    #: purposes this container type serves (a few words only).
+    container_description = u'handles basic applicants'
+
+    @property
+    def local_roles(cls):
+        return []
+
+    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')
+        applicant_id = generate_applicant_id(container=self)
+        applicant.applicant_id = applicant_id
+        self[applicant.application_number] = applicant
+        return
+
+ApplicantsContainer = attrs_to_fields(ApplicantsContainer)
+
+class ApplicantsContainerProvider(grok.GlobalUtility):
+    """A utility that provides basic applicants containers.
+    """
+    grok.implements(IApplicantsContainerProvider)
+    grok.name('waeup.sirp.applicants.ApplicantsContainer')
+
+    #: The applicants container type this provider provides:
+    #: :class:`ApplicantsContainer`.
+    factory = ApplicantsContainer
+
+factory = Factory(lambda : ApplicantsContainer, 'ApplicantsContainer')
+grok.global_utility(factory, IFactory, name='waeup.ApplicantsContainer')
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/dynamicroles.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/dynamicroles.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/dynamicroles.py	(revision 7760)
@@ -0,0 +1,46 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 waeup.sirp.applicants.interfaces import IApplicant
+from waeup.sirp.students.dynamicroles import StudentPrincipalRoleManager
+
+# All components in here have the same context: Applicant instances
+grok.context(IApplicant)
+
+class ApplicantPrincipalRoleManager(StudentPrincipalRoleManager):
+
+    grok.provides(IPrincipalRoleManager)
+
+    #: The attribute name to lookup for additional roles
+    extra_attrib = 'course1'
+    subcontainer = None
+
+    # 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.ApplicationsOfficer',
+        }
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/interfaces.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/interfaces.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/interfaces.py	(revision 7760)
@@ -0,0 +1,620 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 grokcore.content.interfaces import IContainer
+
+from zope import schema
+from zope.interface import Interface, Attribute, implements, directlyProvides
+from zope.component import getUtilitiesFor, queryUtility, getUtility
+from zope.catalog.interfaces import ICatalog
+from zope.schema.interfaces import (
+    ValidationError, ISource, IContextSourceBinder)
+from zc.sourcefactory.basic import BasicSourceFactory
+from zc.sourcefactory.contextual import BasicContextualSourceFactory
+from waeup.sirp.schema import TextLineChoice
+from waeup.sirp.interfaces import (
+    ISIRPObject, year_range, validate_email, academic_sessions_vocab)
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.university.vocabularies import (
+    course_levels, AppCatSource)
+from waeup.sirp.students.vocabularies import (
+    lgas_vocab, CertificateSource, GenderSource)
+from waeup.sirp.payments.interfaces import IOnlinePayment
+
+#: Maximum upload size for applicant passport photographs (in bytes)
+MAX_UPLOAD_SIZE = 1024 * 20
+
+class RegNumInSource(ValidationError):
+    """Registration number exists already
+    """
+    # The docstring of ValidationErrors is used as error description
+    # by zope.formlib.
+    pass
+
+class RegNumberSource(object):
+    implements(ISource)
+    cat_name = 'applicants_catalog'
+    field_name = 'reg_number'
+    validation_error = RegNumInSource
+    def __init__(self, context):
+        self.context = context
+        return
+
+    def __contains__(self, value):
+        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 entry.applicant_id != self.context.applicant_id:
+                # XXX: sources should simply return False.
+                #      But then we get some stupid error message in forms
+                #      when validation fails.
+                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 AppCatCertificateSource(CertificateSource):
+    """An application certificate source delivers all courses which belong to
+    a certain application_category.
+    """
+    def getValues(self, context):
+        # appliction category not available when certificate was deleted.
+        # shouldn't that info be part of applicant info instead?
+        # when we cannot determine the appcat, we will display all courses.
+        appcat = getattr(getattr(context, '__parent__', None),
+                         'application_category', None)
+        catalog = getUtility(ICatalog, name='certificates_catalog')
+        result = catalog.searchResults(
+            application_category=(appcat,appcat))
+        result = sorted(result, key=lambda value: value.code)
+        curr_course = context.course1
+        if curr_course is not None and curr_course not in result:
+            # display also current course even if it is not catalogued
+            # (any more)
+            result = [curr_course,] + result
+        return result
+
+class ApplicationTypeSource(BasicContextualSourceFactory):
+    """An application type source delivers screening types defined in the
+    portal.
+    """
+    def getValues(self, context):
+        appcats_dict = getUtility(
+            IApplicantsUtils).getApplicationTypeDict()
+        return sorted(appcats_dict.keys())
+
+    def getToken(self, context, value):
+        return value
+
+    def getTitle(self, context, value):
+        appcats_dict = getUtility(
+            IApplicantsUtils).getApplicationTypeDict()
+        return appcats_dict[value][0]
+
+# Maybe Uniben 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).getApplicationTypeDict()
+#        return sorted(appcats_dict.keys())
+#
+#    def getToken(self, context, value):
+#        return value
+#
+#    def getTitle(self, context, value):
+#        apppins_dict = getUtility(
+#            IApplicantsUtils).getApplicationTypeDict()
+#        return u"%s (%s)" % (
+#            apppins_dict[value][1],self.apppins_dict[value][0])
+
+class ApplicantContainerProviderSource(BasicSourceFactory):
+    """A source offering all available applicants container types.
+
+    The values returned by this source are names of utilities that can
+    create :class:`ApplicantContainer` instances. So, if you get a
+    name like ``'myactype'`` from this source, then you can do:
+
+      >>> from zope.component import getUtility
+      >>> p = getUtility(IApplicantsContainerProvider, name=myactype)
+      >>> my_applicants_container = p.factory()
+
+    Or you can access class-attributes like
+
+      >>> my_applicants_container.container_title
+      'Pretty'
+
+    """
+    def getValues(self):
+        """Returns a list of ``(<name>, <provider>)`` tuples.
+
+        Here ``<name>`` is the name under which an
+        :class:``ApplicantContainerProvider`` was registered as a
+        utility and ``<provider>`` is the utility itself.
+        """
+        return getUtilitiesFor(IApplicantsContainerProvider)
+
+    def getToken(self, value):
+        """Return the name of the ``(<name>, <provider>)`` tuple.
+        """
+        return value[0]
+
+    def getTitle(self, value):
+        """Get a 'title - description' string for a container type.
+        """
+        factory = value[1].factory
+        return "%s - %s" % (
+            factory.container_title, factory.container_description)
+
+class IApplicantsUtils(Interface):
+    """A collection of methods which are subject to customization.
+    """
+    pass
+
+class IApplicantsRoot(ISIRPObject, IContainer):
+    """A container for university applicants containers.
+    """
+    pass
+
+class IApplicantsContainer(ISIRPObject):
+    """An applicants container contains university applicants.
+
+    """
+
+    container_title = Attribute(
+        u'classattribute: title for type of container')
+    container_description = Attribute(
+        u'classattribute: description for type of container')
+
+
+    code = schema.TextLine(
+        title = _(u'Code'),
+        default = u'-',
+        required = True,
+        readonly = True,
+        )
+
+    title = schema.TextLine(
+        title = _(u'Title'),
+        required = True,
+        default = u'-',
+        readonly = True,
+        )
+
+    prefix = schema.Choice(
+        title = _(u'Application Target'),
+        required = True,
+        default = None,
+        source = ApplicationTypeSource(),
+        readonly = True,
+        )
+
+    entry_level = schema.Choice(
+        title = _(u'Entry Level'),
+        vocabulary = course_levels,
+        default = 100,
+        required = True,
+        )
+
+    year = schema.Choice(
+        title = _(u'Year of Entrance'),
+        required = True,
+        default = None,
+        values = year_range(),
+        readonly = True,
+        )
+
+    provider = schema.Choice(
+        title = _(u'Applicants Container Type'),
+        required = True,
+        default = None,
+        source = ApplicantContainerProviderSource(),
+        readonly = True,
+        )
+
+    # Maybe Uniben 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,
+        default = None,
+        source = AppCatSource(),
+        )
+
+    description = schema.Text(
+        title = _(u'Human readable description in reST format'),
+        required = False,
+        default = u'''This text can been seen by anonymous users.
+Here we put mult-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 = schema.Dict(
+        title = u'Content as language dictionary with values in html format',
+        required = False,
+        default = {},
+        )
+
+    startdate = schema.Date(
+        title = _(u'Application Start Date'),
+        required = False,
+        default = None,
+        )
+
+    enddate = schema.Date(
+        title = _(u'Application Closing Date'),
+        required = False,
+        default = None,
+        )
+
+    strict_deadline = schema.Bool(
+        title = _(u'Forbid additions after deadline (enddate)'),
+        required = True,
+        default = True,
+        )
+
+    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.
+        """
+
+class IApplicantsContainerAdd(IApplicantsContainer):
+    """An applicants container contains university applicants.
+    """
+    prefix = schema.Choice(
+        title = _(u'Application Target'),
+        required = True,
+        default = None,
+        source = ApplicationTypeSource(),
+        readonly = False,
+        )
+
+    year = schema.Choice(
+        title = _(u'Year of Entrance'),
+        required = True,
+        default = None,
+        values = year_range(),
+        readonly = False,
+        )
+
+    provider = schema.Choice(
+        title = _(u'Applicants Container Type'),
+        required = True,
+        default = None,
+        source = ApplicantContainerProviderSource(),
+        readonly = False,
+        )
+
+IApplicantsContainerAdd[
+    'prefix'].order =  IApplicantsContainer['prefix'].order
+IApplicantsContainerAdd[
+    'year'].order =  IApplicantsContainer['year'].order
+IApplicantsContainerAdd[
+    'provider'].order =  IApplicantsContainer['provider'].order
+
+class IApplicantBaseData(ISIRPObject):
+    """The data for an applicant.
+
+    This is a base interface with no field
+    required. For use with importers, forms, etc., please use one of
+    the derived interfaces below, which set more fields to required
+    state, depending on use-case.
+
+    This base interface is also implemented by the StudentApplication
+    class in the students package. Thus, these are the data which are saved
+    after admission.
+    """
+
+    applicant_id = schema.TextLine(
+        title = _(u'Applicant Id'),
+        required = False,
+        readonly = False,
+        )
+    reg_number = TextLineChoice(
+        title = _(u'JAMB Registration Number'),
+        readonly = False,
+        required = True,
+        default = None,
+        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 = schema.Date(
+        title = _(u'Date of Birth'),
+        required = True,
+        )
+    lga = schema.Choice(
+        source = lgas_vocab,
+        title = _(u'State/LGA'),
+        default = 'foreigner',
+        required = True,
+        )
+    sex = schema.Choice(
+        title = _(u'Sex'),
+        source = GenderSource(),
+        default = u'm',
+        required = True,
+        )
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        )
+    phone = schema.TextLine(
+        title = _(u'Phone'),
+        description = u'',
+        required = False,
+        )
+    course1 = schema.Choice(
+        title = _(u'1st Choice Course of Study'),
+        source = CertificateSource(),
+        required = True,
+        )
+    course2 = schema.Choice(
+        title = _(u'2nd Choice Course of Study'),
+        source = CertificateSource(),
+        required = False,
+        )
+
+    #
+    # Data to be imported after screening
+    #
+    screening_score = schema.Int(
+        title = _(u'Screening Score'),
+        required = False,
+        )
+    screening_venue = schema.TextLine(
+        title = _(u'Screening Venue'),
+        required = False,
+        )
+    course_admitted = schema.Choice(
+        title = _(u'Admitted Course of Study'),
+        source = CertificateSource(),
+        default = None,
+        required = False,
+        )
+    notice = schema.Text(
+        title = _(u'Notice'),
+        required = False,
+        )
+
+class IApplicantProcessData(IApplicantBaseData):
+    """An applicant.
+
+    Here we add process attributes and methods to the base data.
+    """
+
+    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('Date of submission, used for export only')
+    password = Attribute('Encrypted password of a applicant')
+    application_number = Attribute('The key under which the record is stored')
+
+    def loggerInfo(ob_class, comment):
+        """Adds an INFO message to the log file
+        """
+
+    student_id = schema.TextLine(
+        title = _(u'Student Id'),
+        required = False,
+        readonly = False,
+        )
+    locked = schema.Bool(
+        title = _(u'Form locked'),
+        default = False,
+        )
+
+class IApplicant(IApplicantProcessData):
+    """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).
+    """
+
+class IApplicantEdit(IApplicantProcessData):
+    """An applicant.
+
+    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.
+    """
+
+    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,
+        )
+    screening_score = schema.Int(
+        title = _(u'Screening Score'),
+        required = False,
+        readonly = True,
+        )
+    screening_venue = schema.TextLine(
+        title = _(u'Screening Venue'),
+        required = False,
+        readonly = True,
+        )
+    course_admitted = schema.Choice(
+        title = _(u'Admitted Course of Study'),
+        source = CertificateSource(),
+        default = None,
+        required = False,
+        readonly = True,
+        )
+    notice = schema.Text(
+        title = _(u'Notice'),
+        required = False,
+        readonly = True,
+        )
+
+    def createStudent():
+        """Create a student object from applicatnt data
+        and copy applicant object.
+        """
+
+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',
+        default = None,
+        required = False,
+        )
+
+class IApplicantOnlinePayment(IOnlinePayment):
+    """An applicant payment via payment gateways.
+
+    """
+    p_year = schema.Choice(
+        title = _(u'Payment Session'),
+        source = academic_sessions_vocab,
+        required = False,
+        )
+
+IApplicantOnlinePayment['p_year'].order = IApplicantOnlinePayment[
+    'p_year'].order
+
+class IApplicantsContainerProvider(Interface):
+    """A provider for applicants containers.
+
+    Applicants container providers are meant to be looked up as
+    utilities. This way we can find all applicant container types
+    defined somewhere.
+
+    Each applicants container provider registered as utility provides
+    one container type and one should be able to call the `factory`
+    attribute to create an instance of the requested container type.
+
+    .. THE FOLLOWING SHOULD GO INTO SPHINX DOCS (and be tested)
+
+    Samples:
+
+    Given, you had an IApplicantsContainer implementation somewhere
+    and you would like to make it findable on request, then you would
+    normally create an appropriate provider utility like this::
+
+      import grok
+      from waeup.sirp.applicants.interfaces import IApplicantsContainerProvider
+
+      class MyContainerProvider(grok.GlobalUtility):
+          grok.implements(IApplicantsContainerProvider)
+          grok.name('MyContainerProvider') # Must be unique
+          factory = MyContainer # A class implementing IApplicantsContainer
+                                # or derivations thereof.
+
+    This utility would be registered on startup and could then be used
+    like this:
+
+      >>> from zope.component import getAllUtilitiesRegisteredFor
+      >>> from waeup.sirp.applicants.interfaces import (
+      ...     IApplicantsContainerProvider)
+      >>> all_providers = getAllUtilitiesRegisteredFor(
+      ...     IApplicantsContainerProvider)
+      >>> all_providers
+      [<MyContainerProvider object at 0x...>]
+
+    You could look up this specific provider by name:
+
+      >>> from zope.component import getUtility
+      >>> p = getUtility(IApplicantsContainerProvider, name='MyProvider')
+      >>> p
+      <MyContainerProvider object at 0x...>
+
+    An applicants container would then be created like this:
+
+      >>> provider = all_providers[0]
+      >>> container = provider.factory()
+      >>> container
+      <MyContainer object at 0x...>
+
+    """
+    factory = Attribute("A class that can create instances of the "
+                        "requested container type")
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/payment.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/payment.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/payment.py	(revision 7760)
@@ -0,0 +1,53 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Acceptance fee payment.
+"""
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.sirp.payments import OnlinePayment
+from waeup.sirp.applicants.interfaces import IApplicantOnlinePayment
+from waeup.sirp.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
+
+ApplicantOnlinePayment = attrs_to_fields(ApplicantOnlinePayment)
+
+# 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/pdf.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/pdf.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/pdf.py	(revision 7760)
@@ -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
+##
+"""
+Generate PDF docs for applicants.
+"""
+import grok
+from datetime import datetime
+from zope.component import getUtility
+from zope.i18n import translate
+from reportlab.pdfgen import canvas
+from reportlab.lib.units import cm
+from reportlab.lib.pagesizes import A4
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.platypus import Frame, Paragraph, Image, Table, Spacer
+from reportlab.platypus.tables import TableStyle
+from zope.component import getUtility
+from zope.formlib.form import setUpEditWidgets
+from zope.publisher.browser import TestRequest
+from waeup.sirp.applicants.interfaces import IApplicant
+from waeup.sirp.browser import DEFAULT_PASSPORT_IMAGE_PATH
+from waeup.sirp.interfaces import IExtFileStore, IPDF, ISIRPUtils
+from waeup.sirp.interfaces import MessageFactory as _
+
+SLIP_STYLE = TableStyle(
+    [('VALIGN',(0,0),(-1,-1),'TOP')]
+    )
+
+
+class PDFApplicationSlip(grok.Adapter):
+
+    grok.context(IApplicant)
+    grok.implements(IPDF)
+    grok.name('application_slip')
+
+    form_fields =  grok.AutoFields(IApplicant).omit(
+        'locked', 'course_admitted')
+
+    @property
+    def title(self):
+        container_title = self.context.__parent__.title
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        ar_translation = translate(_('Application Record'),
+            'waeup.sirp', target_language=portal_language)
+        return '%s - %s %s' % (container_title,
+            ar_translation, self.context.application_number)
+
+    def _setUpWidgets(self):
+        """Setup simple display widgets.
+
+        Returns the list of widgets.
+        """
+        request = TestRequest()
+        return setUpEditWidgets(
+            self.form_fields, 'form', self.context, request, {},
+            for_display=True, ignore_request=True
+            )
+
+    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 _addPassportImage(self, data):
+        """Add passport image to data stream.
+
+        If no image exists yet the default image is added.
+        """
+        img = getUtility(IExtFileStore).getFileByContext(self.context)
+        if img is None:
+            img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
+        doc_img = Image(img.name, width=4*cm, height=3*cm, kind='bound')
+        data.append([doc_img])
+        data.append([Spacer(1, 18)])
+        return data
+
+    def _addDeptAndFaculty(self, data):
+        """If we have a valid course admitted, add dept. and faculty to data
+           stream.
+        """
+        style = getSampleStyleSheet()
+        course_admitted = self.context.course_admitted
+        dept = getattr(
+                getattr(course_admitted, '__parent__', None),
+                '__parent__', None)
+        if dept is None:
+            return data
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        dp_translation = translate(_('Department:'),
+            'waeup.sirp', target_language=portal_language)
+        f_label = '<font size=12>%s</font>' % dp_translation
+        f_text = '<font size=12>%s</font>' % dept.longtitle()
+        f_label = Paragraph(f_label, style["Normal"])
+        f_text = Paragraph(f_text, style["Normal"])
+        data.append([f_label,f_text])
+
+        faculty = getattr(dept, '__parent__', None)
+        if faculty is None:
+            return data
+        fc_translation = translate(_('Faculty:'),
+            'waeup.sirp', target_language=portal_language)
+        f_label = '<font size=12>%s</font>' % fc_translation
+        f_text = '<font size=12>%s</font>' % faculty.longtitle()
+        f_label = Paragraph(f_label, style["Normal"])
+        f_text = Paragraph(f_text, style["Normal"])
+        data.append([f_label,f_text])
+        return data
+
+    def __call__(self, view=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.
+        """
+        pdf = canvas.Canvas('application_slip.pdf',pagesize=A4)
+        pdf.setTitle(self.title)
+        pdf.setSubject('Application')
+        if view is None:
+            pdf.setAuthor('unknown')
+        if view is not None:
+            pdf.setAuthor('%s (%s)' % (view.request.principal.title,
+                view.request.principal.id))
+        pdf.setCreator('SIRP')
+        width, height = A4
+        style = getSampleStyleSheet()
+        pdf.line(1*cm,height-(1.8*cm),width-(1*cm),height-(1.8*cm))
+
+        story = []
+        frame_header = Frame(1*cm,1*cm,width-(1.7*cm),height-(1.7*cm))
+        header_title = getattr(grok.getSite(), 'name', u'Sample University')
+        story.append(Paragraph(header_title, style["Heading1"]))
+        frame_header.addFromList(story,pdf)
+
+        story = []
+        frame_body = Frame(1*cm,1*cm,width-(2*cm),height-(3.5*cm))
+        story.append(Paragraph(self.title, style["Heading2"]))
+        story.append(Spacer(1, 18))
+        for msg in self.context.history.messages:
+            f_msg = '<font face="Courier" size=10>%s</font>' % msg
+            story.append(Paragraph(f_msg, style["Normal"]))
+        story.append(Spacer(1, 24))
+
+        # Setup table data
+        data = []
+        # Insert passport photograph
+        data = self._addPassportImage(data)
+        # Render widget fields
+        widgets = self._setUpWidgets()
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        for widget in widgets: # self.widgets:
+            f_label = '<font size=12>%s</font>:' % translate(
+                widget.label.strip(), 'waeup.sirp',
+                target_language=portal_language)
+            f_label = Paragraph(f_label, style["Normal"])
+            f_text = '<font size=12>%s</font>' % widget()
+            f_text = Paragraph(f_text, style["Normal"])
+            data.append([f_label,f_text])
+        adm_translation = translate(_('Admitted Course of Study:'),
+                'waeup.sirp', target_language=portal_language)
+        f_label = '<font size=12>%s</font>' % adm_translation
+        f_text = '<font size=12>%s</font>' % (
+            self._getCourseAdmittedLink(view),)
+        f_label = Paragraph(f_label, style["Normal"])
+        f_text = Paragraph(f_text, style["Normal"])
+        data.append([f_label,f_text])
+
+        # Add dept. and faculty if applicable
+        data = self._addDeptAndFaculty(data)
+
+        # Create table
+        table = Table(data,style=SLIP_STYLE)
+        story.append(table)
+
+        # Add some comments
+        if self.context.state == 'created':
+            comment1 = _(
+                '<font size=10>Proceed to the login page of the portal' +
+                ' and enter your new credentials:' +
+                ' user name= ${a}, password = ${b}.</font>', mapping = {
+                'a':self.context.student_id, 'b':self.context.application_number}
+                )
+            comment2 = _(
+                '<font size=10>Change your password when you have ' +
+                ' logged in.</font>'
+                )
+            comment1 = Paragraph(comment1, style["Normal"])
+            comment2 = Paragraph(comment2, style["Normal"])
+            story.append(Spacer(1, 18))
+            story.append(comment1)
+            story.append(comment2)
+
+        frame_body.addFromList(story,pdf)
+
+        story = []
+        frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
+        timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
+        f_text = '<font size=10>%s</font>' % timestamp
+        story.append(Paragraph(f_text, style["Normal"]))
+        frame_footer.addFromList(story,pdf)
+        return pdf.getpdfdata()
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/permissions.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/permissions.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/permissions.py	(revision 7760)
@@ -0,0 +1,60 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+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 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.Authenticated', 'waeup.payApplicant')
+
+# Site role
+
+class ApplicantRole(grok.Role):
+    grok.name('waeup.Applicant')
+    grok.permissions('waeup.viewAcademics', 'waeup.viewMyApplicationDataTab')
+
+class ApplicationsOfficer(grok.Role):
+    grok.name('waeup.ApplicationsOfficer')
+    grok.title(u'Applications Officer')
+    grok.permissions('waeup.manageApplication', 'waeup.viewApplication'
+                     'waeup.viewApplicantsTab', 'waeup.payApplicant')
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/root.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/root.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/root.py	(revision 7760)
@@ -0,0 +1,99 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 waeup.sirp.interfaces import ISIRPPluggable
+from waeup.sirp.applicants.interfaces import IApplicantsRoot
+from waeup.sirp.utils.logger import Logger
+
+class ApplicantsRoot(grok.Container, Logger):
+    """The root of applicants-related components. It contains only
+    containers for applicants.
+    """
+    grok.implements(IApplicantsRoot)
+
+    local_roles = []
+
+    logger_name = 'waeup.sirp.${sitename}.applicants'
+    logger_filename = 'applicants.log'
+
+    def logger_info(self, target, ob_class, comment=None):
+        """Get the logger's info method.
+        """
+        self.logger.info('%s - %s - %s' % (
+                target, ob_class, comment))
+        return
+
+class ApplicantsPlugin(grok.GlobalUtility):
+    """A SIRPPlugin that creates an applicants root in portal.
+
+    This plugin should be called by a typical
+    `waeup.sirp.app.Universtiy` instance on creation time. The
+    :meth:`update` method normally can also be triggered manually over
+    the main site configuration.
+
+    Implements :class:`waeup.sirp.interfaces.ISIRPPluggable`
+    """
+    grok.name('applicants')
+    grok.implements(ISIRPPluggable)
+    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):
+            # Applicants up to date. Return.
+            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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/__init__.py	(revision 7760)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/sample_applicant_data.csv
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/sample_applicant_data.csv	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/sample_applicant_data.csv	(revision 7760)
@@ -0,0 +1,4 @@
+container_code,reg_number,firstname,lastname,date_of_birth,email,course1
+dp2011,1001,Aaren,Pieri,1990-01-02,xx@yy.zz,CERT1
+dp2011,1002,Aaren,Finau,1990-01-03,xx@yy.zz,CERT1
+dp2011,1003,Aaren,Berson,1990-01-04,xx@yy.zz,CERT1
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/sample_applicant_data_update.csv
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/sample_applicant_data_update.csv	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/sample_applicant_data_update.csv	(revision 7760)
@@ -0,0 +1,4 @@
+container_code,reg_number,firstname
+dp2011,1001,Aaren
+dp2011,1002,Alfons
+dp2011,1003,Abraham
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/sample_container_data.csv
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/sample_container_data.csv	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/sample_container_data.csv	(revision 7760)
@@ -0,0 +1,3 @@
+code,ac_prefix,application_category,provider,prefix,year
+CODE1,APP,basic,waeup.sirp.applicants.ApplicantsContainer,app,2013
+CODE2,DPP,cest,waeup.sirp.applicants.ApplicantsContainer,app,2013
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/sample_faulty_applicant_data.csv
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/sample_faulty_applicant_data.csv	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/sample_faulty_applicant_data.csv	(revision 7760)
@@ -0,0 +1,2 @@
+container_code,reg_number,firstname,lastname,date_of_birth,email,course1
+dp2011,1001,Aaren,Pieri,01/02/1990,xx@yy.zz,CERT1
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_applicant.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_applicant.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_applicant.py	(revision 7760)
@@ -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
+##
+"""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.sirp.image.interfaces import ISIRPImageFile
+from waeup.sirp.imagestorage import DefaultStorage
+from waeup.sirp.interfaces import IFileStoreHandler, IFileStoreNameChooser
+from waeup.sirp.applicants import (
+    Applicant, ApplicantFactory,
+    ApplicantImageStoreHandler, ApplicantImageNameChooser,
+    )
+from waeup.sirp.applicants.interfaces import IApplicant
+from waeup.sirp.testing import FunctionalTestCase, FunctionalLayer
+
+class FakeImageLocation(object):
+    implements(ILocation)
+    adapts(ISIRPImageFile)
+    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()
+        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_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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_applicantcopier.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_applicantcopier.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_applicantcopier.py	(revision 7760)
@@ -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
+##
+"""
+Tests for the creation of student containers with data from admitted applicants.
+"""
+import os
+from hurry.workflow.interfaces import IWorkflowInfo
+from zope.component import getUtility
+from waeup.sirp.testing import FunctionalLayer
+from waeup.sirp.interfaces import IExtFileStore, IFileStoreNameChooser
+from waeup.sirp.applicants.tests.test_browser import ApplicantsFullSetup
+
+class ApplicantCopierFunctionalTests(ApplicantsFullSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantCopierFunctionalTests, self).setUp()
+        return
+
+    def test_copier(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
+        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.')
+        IWorkflowInfo(self.applicant).fireTransition('start')
+        IWorkflowInfo(self.applicant).fireTransition('pay')
+        IWorkflowInfo(self.applicant).fireTransition('submit')
+        IWorkflowInfo(self.applicant).fireTransition('admit')
+        (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()
+        (success, msg) = self.applicant.createStudent()
+        self.assertTrue('created' in msg)
+        student_id = msg.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)
+        # 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("Search").click()
+        self.assertMatches('...John Tester...', 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
+        IWorkflowInfo(self.applicant).fireTransition('start')
+        IWorkflowInfo(self.applicant).fireTransition('pay')
+        IWorkflowInfo(self.applicant).fireTransition('submit')
+        IWorkflowInfo(self.applicant).fireTransition('admit')
+        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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_authentication.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_authentication.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_authentication.py	(revision 7760)
@@ -0,0 +1,183 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.authentication import get_principal_role_manager
+from waeup.sirp.interfaces import IAuthPluginUtility, IUserAccount
+from waeup.sirp.applicants.authentication import (
+    ApplicantsAuthenticatorSetup, ApplicantAccount)
+from waeup.sirp.applicants.tests.test_browser import ApplicantsFullSetup
+from waeup.sirp.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
+
+
+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 unicode
+        self.assertTrue(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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_batching.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_batching.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_batching.py	(revision 7760)
@@ -0,0 +1,282 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 importers.
+"""
+import datetime
+import os
+import shutil
+import tempfile
+import unittest
+from zope.component.hooks import setSite, clearSite
+from zope.component import createObject
+from zope.interface.verify import verifyClass, verifyObject
+
+from waeup.sirp.app import University
+from waeup.sirp.applicants.batching import (
+    ApplicantsContainerImporter, ApplicantImporter)
+from waeup.sirp.applicants.container import ApplicantsContainer
+from waeup.sirp.applicants.applicant import Applicant
+from waeup.sirp.university.faculty import Faculty
+from waeup.sirp.university.department import Department
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.interfaces import IBatchProcessor
+
+
+# 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 students
+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_HEADER_FIELDS_UPDATE = APPLICANT_SAMPLE_DATA_UPDATE.split(
+    '\n')[0].split(',')
+
+class ApplicantsContainerImporterTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantsContainerImporterTest, 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.importer = ApplicantsContainerImporter()
+        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(ApplicantsContainerImporterTest, 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.importer) is True
+        assert verifyClass(
+            IBatchProcessor, ApplicantsContainerImporter) is True
+
+    def test_parentsExist(self):
+        assert self.importer.parentsExist(None, dict()) is False
+        assert self.importer.parentsExist(None, self.app) is True
+
+    def test_entryExists(self):
+        assert self.importer.entryExists(
+            dict(code='REG_NONE'), self.app) is False
+        assert self.importer.entryExists(
+            dict(code='dp2011'), self.app) is True
+
+    def test_getParent(self):
+        parent = self.importer.getParent(None, self.app)
+        assert parent is self.app['applicants']
+
+    def test_getEntry(self):
+        assert self.importer.getEntry(
+            dict(code='REG_NONE'), self.app) is None
+        assert self.importer.getEntry(
+            dict(code='dp2011'), self.app) is self.container
+
+    def test_addEntry(self):
+        self.importer.addEntry(
+            'New application', dict(code='dp2012'), self.app)
+        assert self.app['applicants']['dp2012'] == 'New application'
+
+    def test_delEntry(self):
+        self.importer.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.importer.doImport(
+            self.csv_file, APPS_CONTAINER_HEADER_FIELDS)
+        avail_containers = [x for x in self.app['applicants'].keys()]
+        self.assertTrue(u'CODE1' in avail_containers)
+        self.assertTrue(u'CODE2' in avail_containers)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+class ApplicantImporterTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantImporterTest, 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.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)
+
+        # Add applicant with subobjects
+        applicant = Applicant()
+        applicant.firstname = u'Anna'
+        applicant.firstname = u'Tester'
+        self.app['applicants']['dp2011'].addApplicant(applicant)
+        self.application_number = applicant.application_number
+        self.applicant = self.app['applicants']['dp2011'][
+            self.application_number]
+        self.importer = ApplicantImporter()
+        self.workdir = tempfile.mkdtemp()
+        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')
+        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)
+
+    def tearDown(self):
+        super(ApplicantImporterTest, 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.importer) is True
+        assert verifyClass(
+            IBatchProcessor, ApplicantImporter) is True
+
+    def test_entryExists(self):
+        assert self.importer.entryExists(
+            dict(container_code='dp2011', application_number='999'),
+            self.app) is False
+
+    def test_getEntry(self):
+        applicant = self.importer.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.importer.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.importer.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.importer.doImport(
+            self.csv_file, APPLICANT_HEADER_FIELDS)
+        self.assertEqual(num_warns,0)
+        assert len(self.app['applicants']['dp2011'].keys()) == 4
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_faulty(self):
+        # 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
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file_faulty, APPLICANT_HEADER_FIELDS)
+        if fail_file is not None:
+            fail_contents = open(fail_file, 'rb').read()
+            shutil.rmtree(os.path.dirname(fail_file))
+        else:
+            shutil.rmtree(os.path.dirname(fin_file))
+        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)')
+        return
+
+    def test_import_update(self):
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file, APPLICANT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file_update, APPLICANT_HEADER_FIELDS_UPDATE, '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.importer.doImport(
+            self.csv_file, APPLICANT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file_update, APPLICANT_HEADER_FIELDS_UPDATE, 'remove')
+        self.assertEqual(num_warns,0)
+        shutil.rmtree(os.path.dirname(fin_file))
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_browser.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_browser.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_browser.py	(revision 7760)
@@ -0,0 +1,788 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 shutil
+import tempfile
+from StringIO import StringIO
+from datetime import datetime, date, timedelta
+from mechanize import LinkNotFoundError
+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.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.app import University
+from waeup.sirp.configuration import SessionConfiguration
+from waeup.sirp.applicants.container import ApplicantsContainer
+from waeup.sirp.applicants.applicant import Applicant
+from waeup.sirp.interfaces import (
+    IExtFileStore, IFileStoreNameChooser, IUserAccount)
+from waeup.sirp.university.faculty import Faculty
+from waeup.sirp.university.department import Department
+
+PH_LEN = 2059  # Length of placeholder file
+
+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.manage_root_path = self.root_path + '/@@manage'
+        self.add_container_path = self.root_path + '/@@add'
+        self.container_path = 'http://localhost/app/applicants/app2009'
+        self.manage_container_path = self.container_path + '/@@manage'
+
+        # Add an applicants container
+        applicantscontainer = ApplicantsContainer()
+        applicantscontainer.code = u'app2009'
+        applicantscontainer.prefix = 'app'
+        applicantscontainer.year = 2009
+        applicantscontainer.application_category = 'basic'
+        delta = timedelta(days=10)
+        applicantscontainer.startdate = date.today() - delta
+        applicantscontainer.enddate = date.today() + delta
+        self.app['applicants']['app2009'] = applicantscontainer
+        self.applicantscontainer = self.app['applicants']['app2009']
+
+        # Populate university
+        certificate = createObject('waeup.Certificate')
+        certificate.code = 'CERT1'
+        certificate.application_category = 'basic'
+        certificate.start_level = 100
+        certificate.end_level = 500
+        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='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 = 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'
+        app['applicants']['app2009'].addApplicant(self.applicant)
+        IUserAccount(
+            self.app['applicants']['app2009'][
+            self.applicant.application_number]).setPassword('apwd')
+        self.manage_path = 'http://localhost/app/applicants/%s/%s/%s' % (
+            'app2009', self.applicant.application_number, 'manage')
+        self.edit_path = 'http://localhost/app/applicants/%s/%s/%s' % (
+            'app2009', self.applicant.application_number, 'edit')
+        self.view_path = 'http://localhost/app/applicants/%s/%s' % (
+            'app2009', 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.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.lga").value = ['foreigner']
+        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_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
+
+    # We have no local roles yet
+    #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(self.manage_root_path)
+    #    self.browser.getControl(name="user").value = ['bob']
+    #    self.browser.getControl(name="local_role").value = [
+    #        'waeup.local.ApplicationsOfficer']
+    #    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.ApplicationsOfficer').selected = True
+    #    self.browser.getControl("Remove selected local roles").click()
+    #    self.assertTrue('Successfully removed:' 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("Cancel").click()
+        self.assertEqual(self.browser.url, self.root_path)
+        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 = ['2010']
+        self.browser.getControl(name="form.provider").value = [
+            'waeup.sirp.applicants.ApplicantsContainer']
+        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("app2010").click()
+        self.assertTrue('<h1>General Studies 2010/2011</h1>'
+            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 = ['2010']
+        self.browser.getControl(name="form.provider").value = [
+            'waeup.sirp.applicants.ApplicantsContainer']
+        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='app2010').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 = ['2010']
+        self.browser.getControl(name="form.provider").value = [
+            'waeup.sirp.applicants.ApplicantsContainer']
+        #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']['app2010']
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value='app2010').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("Save").click()
+        self.assertTrue('Form has been saved' in self.browser.contents)
+        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_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.getControl("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)
+        # Open pdf slip
+        self.browser.open(self.slip_path)
+        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_edit_applicant(self):
+        # Applicants can edit their record
+        self.browser.open(self.login_path)
+        self.login()
+        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.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...', self.browser.contents)
+        return
+
+    def image_url(self, filename):
+        return self.edit_path.replace('edit', filename)
+
+    def test_after_login_default_browsable(self):
+        # After login we see the placeholder image in the edit view
+        self.login()
+        self.assertEqual(self.browser.url, self.view_path)
+        self.browser.open(self.edit_path)
+        # There is a correct <img> link included
+        self.assertTrue(
+              '<img src="passport.jpg" />' 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" />' 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" />' 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" />' 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" />' 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)
+
+    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_pay_acceptance_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 = 2009
+        configuration.acceptance_fee = 200
+        self.app['configuration'].addSessionConfiguration(configuration)
+        self.browser.open(self.edit_path)
+        self.fill_correct_values()
+        self.browser.getControl("Add online payment ticket").click()
+        self.assertMatches('...Payment ticket created...',
+                           self.browser.contents)
+        # Payment ticket can be removed if they haven't received a
+        # valid callback
+        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()
+        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
+        # The pdf payment receipt can't yet be opened
+        self.browser.open(payment_url + '/payment_receipt.pdf')
+        self.assertMatches('...Ticket not yet paid...',
+                           self.browser.contents)
+        # Request callback
+        self.browser.open(payment_url)
+        self.browser.getLink("Request callback").click()
+        self.assertMatches('...Valid callback received...',
+                          self.browser.contents)
+        # Callback can't be applied twice
+        self.browser.open(payment_url + '/callback')
+        self.assertMatches(
+            "...Transition 'pay' requires 'started' as source state...",
+            self.browser.contents)
+        # The payment receipt can be downloaded now
+        self.browser.open(payment_url)
+        self.browser.getLink("Download payment receipt").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'],
+                         'application/pdf')
+        # Applicant is is in state 'paid'
+        self.browser.open(self.view_path)
+        self.assertMatches('...paid...',
+                           self.browser.contents)
+        state = IWorkflowState(self.applicant).getState()
+        self.assertTrue(state == 'paid')
+
+    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("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
+        self.browser.getControl("Final Submit").click()
+        self.assertTrue(
+            '... submitted ...' in self.browser.contents)
+        self.browser.goBack(count=1)
+        self.browser.getControl("Save").click()
+        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(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 = 'John'
+        self.browser.getControl(name="form.lastname").value = 'Tester'
+        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.extension").value = '6666666'
+        self.browser.getControl("Get login credentials").click()
+        self.assertEqual(self.browser.url,
+            self.container_path + '/registration_complete?email=xx%40yy.zz')
+        return
+
+    def test_register_applicant_wo_phone(self):
+        # We don't require the phone number when registering
+        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 = 'John'
+        self.browser.getControl(name="form.lastname").value = 'Tester'
+        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
+        self.browser.getControl("Get login credentials").click()
+        self.assertEqual(self.browser.url,
+            self.container_path + '/registration_complete?email=xx%40yy.zz')
+        return
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_catalog.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_catalog.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_catalog.py	(revision 7760)
@@ -0,0 +1,87 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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
+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.sirp.app import University
+from waeup.sirp.applicants.container import ApplicantsContainer
+from waeup.sirp.applicants.applicant import Applicant
+from waeup.sirp.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)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_container.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_container.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_container.py	(revision 7760)
@@ -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.sirp.applicants import interfaces
+from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_dynamicroles.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_dynamicroles.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_dynamicroles.py	(revision 7760)
@@ -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.sirp.testing import FunctionalLayer
+from waeup.sirp.applicants.tests.test_browser import ApplicantsFullSetup
+from waeup.sirp.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
+        self.officer_role = 'waeup.ApplicationsOfficer'
+        # assign clearance permissions for a virtual officer
+        prm = IPrincipalRoleManager(self.department)
+        prm.assignRoleToPrincipal('waeup.local.ClearanceOfficer', '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 clearance role on the connected dept
+        # we get the ApplicationsOfficer role on our applicant
+        prm = IPrincipalRoleManager(self.applicant)
+        result = prm.getRolesForPrincipal('alice')
+        self.assertEqual(result, [('waeup.ApplicationsOfficer', Allow)])
+        return
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_interfaces.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_interfaces.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_interfaces.py	(revision 7760)
@@ -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
+##
+"""
+Test components defined in interface module.
+"""
+import unittest
+from zc.sourcefactory.browser.source import FactoredTerms
+from zope.publisher.browser import TestRequest
+from waeup.sirp.applicants.interfaces import ApplicationTypeSource
+from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_payment.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_payment.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_payment.py	(revision 7760)
@@ -0,0 +1,46 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.
+
+"""
+
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.applicants.payment import (
+    ApplicantOnlinePayment, ApplicantOnlinePaymentFactory)
+from waeup.sirp.payments.interfaces import IOnlinePayment
+
+class ApplicantOnlinePaymentFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantOnlinePaymentFactoryTest, self).setUp()
+        self.factory = ApplicantOnlinePaymentFactory()
+        return
+
+    def tearDown(self):
+        super(ApplicantOnlinePaymentFactoryTest, self).tearDown()
+        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)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_pdf.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_pdf.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_pdf.py	(revision 7760)
@@ -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.sirp.applicants.pdf import IPDF, PDFApplicationSlip
+from waeup.sirp.applicants.tests.test_browser import ApplicantsFullSetup
+from waeup.sirp.interfaces import IPDF
+from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_root.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_root.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/tests/test_root.py	(revision 7760)
@@ -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.sirp.app import University
+from waeup.sirp.applicants import (
+    interfaces, Applicant, ApplicantsContainer,
+    )
+from waeup.sirp.applicants.root import (
+    ApplicantsRoot, ApplicantsPlugin,
+    )
+from waeup.sirp.testing import (
+    FunctionalLayer, FunctionalTestCase, remove_logger)
+
+
+class FakeSite(dict):
+    pass
+
+class ApplicantsRootTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        remove_logger('waeup.sirp.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.sirp.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.sirp.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.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/utils.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/utils.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/utils.py	(revision 7760)
@@ -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
+##
+"""General helper functions and utilities for the application section.
+"""
+
+import grok
+from waeup.sirp.applicants.interfaces import IApplicantsUtils
+
+class ApplicantsUtils(grok.GlobalUtility):
+    """A collection of parameters and methods subject to customization.
+    """
+    grok.implements(IApplicantsUtils)
+
+    def getApplicationTypeDict(self):
+        """Provide a dictionary of application types.
+        """
+        return {
+        'pude': ['Post UDE Screening', 'PUDE'],
+        'prence': ['Pre-NCE Programme', 'PRE'],
+        'sandwich': ['Part Time Degree in Education', 'SAND'],
+        'pt': ['Part-Time Degree Programmes', 'PTP'],
+        'pume': ['Post UME Screening Test', 'PUME'],
+        'app': ['General Studies', 'APP'],
+        'cest': ['Common Entry Screening Test', 'CEST'],
+        'ct': ['Certificate Programmes', 'CTP'],
+        'dp': ['Diploma Programmes', 'DPP'],
+        'pce': ['PCE Screening', 'PCE']
+        }
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/viewlets.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/viewlets.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/viewlets.py	(revision 7760)
@@ -0,0 +1,190 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.workflow.interfaces import IWorkflowState
+from waeup.sirp.interfaces import ISIRPObject
+from waeup.sirp.students.viewlets import PrimaryStudentNavTab
+from waeup.sirp.browser.viewlets import ManageActionButton, PrimaryNavTab
+from waeup.sirp.applicants.interfaces import (
+    IApplicant, IApplicantsRoot, IApplicantsContainer,
+    IApplicantOnlinePayment,
+    )
+from waeup.sirp.applicants.browser import (
+    ApplicantsRootPage, ApplicantsContainerPage, ApplicantManageFormPage,
+    ApplicantDisplayFormPage, OnlinePaymentDisplayFormPage
+    )
+
+from waeup.sirp.interfaces import MessageFactory as _
+
+grok.context(ISIRPObject) # Make ISIRPObject the default context
+grok.templatedir('browser_templates')
+
+class ApplicantsAuthTab(PrimaryNavTab):
+    """Applicants tab in primary navigation.
+    """
+    grok.context(ISIRPObject)
+    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 ManageApplicantsRootActionButton(ManageActionButton):
+    grok.context(IApplicantsRoot)
+    grok.view(ApplicantsRootPage)
+    grok.require('waeup.manageApplication')
+    text = _('Manage application section')
+
+class ApplicantsContainerManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsContainerPage)
+    grok.require('waeup.manageApplication')
+    text = _('Manage applicants container')
+
+class ApplicantRegisterActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsContainerPage)
+    grok.require('waeup.Anonymous')
+    icon = 'actionicon_login.png'
+    text = _('Register for application')
+    target = 'register'
+
+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:
+            return
+        return self.view.url(self.view.context, self.target)
+
+class PDFActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(IApplicant)
+    grok.require('waeup.viewApplication')
+    icon = 'actionicon_pdf.png'
+    text = _('Download application slip')
+    target = 'application_slip.pdf'
+
+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 IWorkflowState(self.context).getState() != 'admitted':
+            return
+        return self.view.url(self.view.context, self.target)
+
+class PaymentReceiptActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IApplicantOnlinePayment)
+    grok.view(OnlinePaymentDisplayFormPage)
+    grok.require('waeup.viewApplication')
+    icon = 'actionicon_pdf.png'
+    text = _('Download payment receipt')
+    target = 'payment_receipt.pdf'
+
+    @property
+    def target_url(self):
+        if self.context.p_state != 'paid':
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class RequestCallbackActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IApplicantOnlinePayment)
+    grok.view(OnlinePaymentDisplayFormPage)
+    grok.require('waeup.payApplicant')
+    icon = 'actionicon_call.png'
+    text = _('Request callback')
+    target = 'callback'
+
+    @property
+    def target_url(self):
+        if self.context.p_state != 'unpaid':
+            return ''
+        return self.view.url(self.view.context, self.target)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/workflow.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/workflow.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/applicants/workflow.py	(revision 7760)
@@ -0,0 +1,173 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.applicants.interfaces import IApplicantBaseData
+from waeup.sirp.interfaces import IObjectHistory, ISIRPWorkflowInfo, ISIRPUtils
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.workflow import SIRPWorkflow, SIRPWorkflowInfo
+
+INITIALIZED = 'initialized'
+STARTED = 'started'
+PAID = 'paid'
+SUBMITTED = 'submitted'
+ADMITTED = 'admitted'
+NOT_ADMITTED = 'not admitted'
+CREATED = 'created'
+
+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 acceptance fee'),
+        msg = _('Fee paid'),
+        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'),
+        source = SUBMITTED,
+        destination = PAID),
+    )
+
+application_workflow = SIRPWorkflow(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(SIRPWorkflowInfo, grok.Adapter):
+    """Adapter to adapt Applicant objects to workflow info objects.
+    """
+    grok.context(IApplicantBaseData)
+    grok.provides(ISIRPWorkflowInfo)
+
+    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']
+    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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/authentication.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/authentication.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/authentication.py	(revision 7760)
@@ -0,0 +1,353 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 SIRP.
+"""
+import grok
+from zope.event import notify
+from zope.component import getUtility, getUtilitiesFor
+from zope.interface import Interface
+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.sirp.interfaces import (ILocalRoleSetEvent,
+    IUserAccount, IAuthPluginUtility, IPasswordValidator,
+    ISIRPPrincipal, ISIRPPrincipalInfo)
+
+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 SIRPSessionCredentialsPlugin(grok.GlobalUtility,
+                                    SessionCredentialsPlugin):
+    grok.provides(ICredentialsPlugin)
+    grok.name('credentials')
+
+    loginpagename = 'login'
+    loginfield = 'form.login'
+    passwordfield = 'form.password'
+
+class SIRPPrincipalInfo(object):
+    """An implementation of ISIRPPrincipalInfo.
+
+    A SIRP principal info is created with id, login, title, description,
+    phone, email and user_type.
+    """
+    grok.implements(ISIRPPrincipalInfo)
+
+    def __init__(self, id, title, description, email, phone, user_type):
+        self.id = id
+        self.title = title
+        self.description = description
+        self.email = email
+        self.phone = phone
+        self.user_type = user_type
+        self.credentialsPlugin = None
+        self.authenticatorPlugin = None
+
+class SIRPPrincipal(Principal):
+    """A portal principal.
+
+    SIRP principals provide an extra `email`, `phone` and `user_type`
+    attribute extending ordinary principals.
+    """
+
+    grok.implements(ISIRPPrincipal)
+
+    def __init__(self, id, title=u'', description=u'', email=u'',
+                 phone=None, 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.user_type = user_type
+
+    def __repr__(self):
+        return 'SIRPPrincipal(%r)' % self.id
+
+class AuthenticatedSIRPPrincipalFactory(grok.MultiAdapter):
+    """Creates 'authenticated' SIRP principals.
+
+    Adapts (principal info, request) to a SIRPPrincipal instance.
+
+    This adapter is used by the standard PAU to transform
+    SIRPPrincipalInfos into SIRPPrincipal instances.
+    """
+    grok.adapts(ISIRPPrincipalInfo, IRequest)
+    grok.implements(IAuthenticatedPrincipalFactory)
+
+    def __init__(self, info, request):
+        self.info = info
+        self.request = request
+
+    def __call__(self, authentication):
+        principal = SIRPPrincipal(
+            self.info.id,
+            self.info.title,
+            self.info.description,
+            self.info.email,
+            self.info.phone,
+            self.info.user_type,
+            authentication.prefix,
+            )
+        notify(
+            AuthenticatedPrincipalCreated(
+                authentication, principal, self.info, self.request))
+        return principal
+
+class Account(grok.Model):
+    grok.implements(IUserAccount)
+
+    _local_roles = dict()
+
+    def __init__(self, name, password, title=None, description=None,
+                 email=None, phone=None, roles = []):
+        self.name = name
+        if title is None:
+            title = name
+        self.title = title
+        self.description = description
+        self.email = email
+        self.phone = phone
+        self.setPassword(password)
+        self.setSiteRolesForPrincipal(roles)
+        # We don't want to share this dict with other accounts
+        self._local_roles = dict()
+
+    def setPassword(self, password):
+        passwordmanager = getUtility(IPasswordManager, 'SHA1')
+        self.password = passwordmanager.encodePassword(password)
+
+    def checkPassword(self, password):
+        passwordmanager = getUtility(IPasswordManager, 'SHA1')
+        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
+        if not account.checkPassword(credentials['password']):
+            return None
+        return SIRPPrincipalInfo(
+            id=account.name,
+            title=account.title,
+            description=account.description,
+            email=account.email,
+            phone=account.phone,
+            user_type=u'user')
+
+    def principalInfo(self, id):
+        account = self.getAccount(id)
+        if account is None:
+            return None
+        return SIRPPrincipalInfo(
+            id=account.name,
+            title=account.title,
+            description=account.description,
+            email=account.email,
+            phone=account.phone,
+            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 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 role manager, no roles to remove
+                continue
+            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
+    role_id = event.role_id
+    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
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/authentication.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/authentication.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/authentication.txt	(revision 7760)
@@ -0,0 +1,207 @@
+SIRP 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.sirp.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.sirp.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.sirp.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.sirp.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.sirp.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.sirp.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.sirp...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.sirp.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.
+
+
+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 wrong 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/__init__.py	(revision 7760)
@@ -0,0 +1,15 @@
+import os
+
+from waeup.sirp.browser.layout import (
+    SIRPPage, SIRPForm, SIRPLayout, SIRPDisplayFormPage, SIRPEditFormPage,
+    SIRPAddFormPage, NullValidator)
+from waeup.sirp.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_m.jpg')
+DEFAULT_PASSPORT_IMAGE_PATH = os.path.join(IMAGE_PATH, 'placeholder_m.jpg')
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/batchprocessing.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/batchprocessing.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/batchprocessing.txt	(revision 7760)
@@ -0,0 +1,578 @@
+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.sirp.app import University
+    >>> getRootFolder()['app'] = University()
+
+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:
+
+    >>> open('faculties.csv', 'wb').write(
+    ... """code,title,title_prefix
+    ... FAC1,Faculty 1,faculty
+    ... FAC2,Faculty 2,institute
+    ... FAC3,Faculty 3,school
+    ... """)
+
+Upload the file:
+
+    >>> import cStringIO
+    >>> browser.getLink('Upload CSV file').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('Batch processing').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
+    ['Applicant Importer',
+     'Applicants Container Importer',
+     'CourseTicket Importer',
+     'Payment Importer',
+     'Student Importer',
+     'StudentStudyCourse Importer (update only)',
+     'StudentStudyLevel Importer',
+     'CertificateCourse Importer',
+     'Certificate Importer',
+     'Course Importer',
+     'Department Importer',
+     'Faculty Importer']
+
+    >>> importerselect.getControl('Faculty Importer').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.getControl('View processing log').click()
+    >>> print browser.contents
+    <!DOCTYPE ...
+    ...<h1>Logfile datacenter.log</h1>...
+
+    >>> browser.getControl(name='query').value = "zope.mgr"
+    >>> browser.getControl('Search').click()
+    >>> 'zope.mgr: Batch processing finished: OK' in browser.contents
+    True
+    >>> 'Mode: create' in browser.contents
+    True
+
+    >>> browser.getControl(name='query').value = "finished"
+    >>> browser.getControl('Search').click()
+    >>> 'zope.mgr: Batch processing finished: OK' in browser.contents
+    True
+    >>> 'Mode: create' in browser.contents
+    False
+
+
+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 CSV file').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('Batch processing').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 Importer').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 CSV file').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('Batch processing').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 Importer', index=2).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,ug_ft,100,300,basic
+    ... CERT2,FAC1,DEP1,Certificate 2,ug_ft,200,300,cest
+    ... """)
+
+Upload the file:
+
+    >>> import cStringIO
+    >>> browser.getLink('Upload CSV file').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('Batch processing').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 Importer').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 CSV file').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('Batch processing').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 Importer').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...
+
+
+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 CSV file').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()
+
+Step 1: start batch processing:
+
+    >>> browser.getLink('Batch processing').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 Importer').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 in the same container. 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('Batch processing').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 Importer').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))
+    ['finished', 'logs', 'unfinished']
+
+    >>> os.listdir(dc_path + '/unfinished')
+    []
+
+    >>> sorted(os.listdir(dc_path + '/finished'))
+    ['certificates_zope.mgr.create.finished.csv', ..., 
+     'newfaculties_zope.mgr.create.finished.csv', 
+     'newfaculties_zope.mgr.csv']
+
+
+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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/breadcrumbs.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/breadcrumbs.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/breadcrumbs.py	(revision 7760)
@@ -0,0 +1,260 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.interfaces import (
+    IConfigurationContainer, ISessionConfiguration)
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.browser import interfaces
+from waeup.sirp.browser.interfaces import (IBreadcrumb,
+    IBreadcrumbIgnorable, IBreadcrumbContainer)
+        
+class Breadcrumb(grok.Adapter):
+    """A most general breadcrumb generator.
+    """
+    grok.provides(IBreadcrumb)
+    grok.context(interfaces.ISIRPObject)
+    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(interfaces.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(interfaces.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(interfaces.IFacultiesContainer)
+    title = _(u'Academics')
+
+class AdministrationBreadcrumb(Breadcrumb):
+    """A breadcrumb for administration areas of University instances.
+    """
+    grok.context(interfaces.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(interfaces.IUsersContainer)
+    title = _(u'Portal Users')
+    parent_viewname = 'administration'
+
+class DataCenterBreadcrumb(Breadcrumb):
+    """A breadcrumb for data centers.
+    """ 
+    grok.context(interfaces.IDataCenter)
+    title = _(u'Data Center')
+    parent_viewname = 'administration'
+
+class FacultyBreadcrumb(Breadcrumb):
+    """A breadcrumb for faculties.
+    """
+    grok.context(interfaces.IFaculty)
+    
+    @property
+    def title(self):
+        return self.context.longtitle()
+
+class DepartmentBreadcrumb(FacultyBreadcrumb):
+    """A breadcrumb for departments.
+    """
+    grok.context(interfaces.IDepartment)
+    
+class CourseBreadcrumb(FacultyBreadcrumb):
+    """A breadcrumb for courses.
+    """
+    grok.context(interfaces.ICourse)   
+    
+class CertificateBreadcrumb(FacultyBreadcrumb):
+    """A breadcrumb for certificates.
+    """
+    grok.context(interfaces.ICertificate)        
+
+class CoursesContainerBreadcrumb(Breadcrumb):
+    """ We don't want course container breadcrumbs.
+    """
+    grok.context(interfaces.ICoursesContainer)
+    grok.implements(IBreadcrumbIgnorable)
+
+class CertificatesContainerBreadcrumb(Breadcrumb):
+    """ We don't want course container breadcrumbs.
+    """
+    grok.context(interfaces.ICertificatesContainer)
+    grok.implements(IBreadcrumbIgnorable)
+
+class CertificateCourseBreadcrumb(Breadcrumb):
+    """ We don't want course container breadcrumbs.
+    """
+    grok.context(interfaces.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/breadcrumbs.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/breadcrumbs.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/breadcrumbs.txt	(revision 7760)
@@ -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.sirp.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.sirp.browser.breadcrumbs import getBreadcrumbList
+    >>> blist1 = getBreadcrumbList(app, 'index')
+    >>> blist1
+    [<waeup.sirp.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.sirp.browser.breadcrumbs.UniversityBreadcrumb object at 0x...>,
+     <waeup.sirp.browser.breadcrumbs.AdministrationBreadcrumb object at 0x...>,
+     <waeup.sirp.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.sirp.app.University object at 0x...>, 'index'),
+      (<waeup.sirp.app.University object at 0x...>, 'administration'),
+      (<waeup.sirp.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.sirp.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.sirp.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.sirp.browser.interfaces import IBreadcrumb
+    >>> b1 = getAdapter(app, IBreadcrumb, 'index')
+    >>> b1
+    <waeup.sirp.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.sirp.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.sirp.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.sirp...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.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/browser.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/browser.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/browser.txt	(revision 7760)
@@ -0,0 +1,1297 @@
+Browsing SIRP
+*************
+
+Here we visit all parts of a SIRP 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.sirp.app import University
+  >>> u = University()
+  >>> root['myuniversity'] = u
+  >>> list(root)
+  [u'myuniversity']
+
+  >>> from zope.component.hooks import setSite
+  >>> setSite(root['myuniversity'])
+
+To make sure, we can 'watch' pages, we first have to initialize out
+test browser::
+
+  >>> from zope.testbrowser.testing import Browser
+  >>> browser = Browser()
+
+Let's get the default view of a university::
+
+  >>> browser.open('http://localhost/myuniversity')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Welcome to WAeUP.SIRP...
+  ...
+
+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...
+  ...
+
+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"...
+  ...Settings have 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 ReST 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 the Student Information and Registration Portal...</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 im Studenteninformations- und Registrierungsportal</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="password").value = 'secret'
+  >>> browser.getControl(name="control_password").value = 'secret'
+  >>> browser.getControl(name="form.email").value = 'xx@yy.zz'
+  >>> browser.getControl(name="form.phone").value = '1234'
+  >>> 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>Applications Officer <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="password").value = 'secret'
+  >>> browser.getControl(name="control_password").value = 'secret'
+  >>> browser.getControl(name="form.email").value = 'xx@yy.zz'
+  >>> browser.getControl(name="form.phone").value = '1234'
+  >>> 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").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>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>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>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>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.sirp.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
+  ['First Semester', 'Second 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
+
+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.sirp.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 = ['ume_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 = ['ume_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 = ['ume_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 = ['ume_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 course referrer`` 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 course referrer').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
+  ['(no value)', '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 course referrer').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 course referrer').click()
+  >>> ctrl = browser.getControl(name='form.course')
+  >>> ctrl.getControl('COURSE1').selected = True
+  >>> browser.getControl(name='form.level').value = ['100']
+  >>> browser.getControl('Add course referrer').click()
+  >>> 'The chosen course referrer 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 course referrer').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 course referrer').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 course referrers').click()
+  >>> 'No item selected' in browser.contents
+  True
+  >>> browser.getControl(name='val_id').getControl(
+  ...    value='COURSE1_200').selected = True
+  >>> browser.getControl('Remove selected course referrers').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))
+  ['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 CSV file').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))
+  ['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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/captcha.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/captcha.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/captcha.py	(revision 7760)
@@ -0,0 +1,343 @@
+## $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.sirp.browser import SIRPPage
+from waeup.sirp.browser.interfaces import (
+    ICaptchaRequest, ICaptchaResponse, ICaptcha, ICaptchaConfig,
+    ICaptchaManager)
+from waeup.sirp.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 SIRP 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'
+
+    # 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 SIRP",
+                }
+            )
+        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).
+        """
+        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(SIRPPage):
+    # 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/exceptions.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/exceptions.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/exceptions.py	(revision 7760)
@@ -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.sirp.browser.layout import SIRPPage
+
+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.s. 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(SIRPPage):
+    """A page rendered when an object cannot be found.
+
+    XXX: This page won't work for objects above a w.s.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/interfaces.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/interfaces.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/interfaces.py	(revision 7760)
@@ -0,0 +1,164 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Interfaces for viewing components.
+"""
+from zope import schema
+from zope.interface import Interface, Attribute
+from waeup.sirp.interfaces import (
+    ISIRPObject, IUniversity, IUsersContainer, IDataCenter)
+from waeup.sirp.university.interfaces import (
+    IFacultiesContainer, IFaculty, IFacultyAdd, IDepartment, IDepartmentAdd,
+    ICoursesContainer, ICourse, ICourseAdd, ICertificatesContainer,
+    ICertificate, ICertificateAdd, ICertificateCourse, ICertificateCourseAdd)
+
+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.sirp.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.
+        """
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/layout.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/layout.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/layout.py	(revision 7760)
@@ -0,0 +1,318 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.interfaces import ISIRPObject, IUserAccount
+from waeup.sirp.browser.interfaces import ITheme
+from waeup.sirp.browser.theming import get_all_themes, SIRPThemeBase
+from waeup.sirp.students.interfaces import IStudentNavigation
+from waeup.sirp.applicants.interfaces import IApplicant
+from waeup.sirp.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'
+
+class action(grok.action):
+
+    def __call__(self, success):
+        action = SIRPAction(self.label, success=success, **self.options)
+        self.actions.append(action)
+        return action
+
+class SIRPAction(Action):
+
+    def __init__(self, label, style='', **options):
+        super(SIRPAction, 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)
+        return ('<input type="submit" id="%s" name="%s" value="%s"'
+                ' class="btn" onclick="return confirmPost(%s)" />' %
+                (self.__name__, self.__name__, escape(label, quote=True), self.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 SIRPLayout(UtilityView,Layout):
+    """A megrok.layout.Layout with additional methods.
+    """
+    grok.baseclass()
+
+class SIRPForm(UtilityView,Form):
+    """A megrok.layout.Form with additional methods.
+    """
+    grok.baseclass()
+
+    def setUpWidgets(self,ignore_request=False):
+        super(SIRPForm,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 SIRPPage(UtilityView,Page):
+    """A megrok.layout page with additional methods.
+    """
+    grok.baseclass()
+
+class SIRPDisplayFormPage(UtilityView,DisplayForm):
+    """A megrok.layout.DisplayForm with additional methods.
+    """
+    grok.baseclass()
+    template = default_waeup_display_template
+
+class SIRPEditFormPage(UtilityView,EditForm):
+    """A megrok.layout.EditForm with additional methods.
+    """
+    grok.baseclass()
+    template = default_waeup_edit_template
+
+    def setUpWidgets(self,ignore_request=False):
+        super(SIRPEditFormPage,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 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('notice'):
+            self.widgets['notice'].height = 3
+        if self.widgets.get('perm_address'):
+            self.widgets['perm_address'].cssClass = 'span8'
+            self.widgets['perm_address'].height = 10
+
+class SIRPAddFormPage(UtilityView,AddForm):
+    """A megrok.layout.AddForm with additional methods.
+    """
+    grok.baseclass()
+    template = default_waeup_edit_template
+
+class SiteLayout(SIRPLayout):
+    """ The general site layout.
+    """
+    grok.context(ISIRPObject)
+
+    #: An instance of the default theme to use for the site layout
+    default_theme = SIRPThemeBase()
+    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 IStudentNavigation.providedBy(self.context):
+            return self.context.getStudent().display_fullname
+        return
+
+    def getApplicantName(self):
+        """Return the applicant name.
+        """
+        if IApplicant.providedBy(self.context):
+            return self.context.fullname
+        return
+
+    def formatDatetime(self,datetimeobj):
+        if isinstance(datetimeobj, datetime):
+            return datetimeobj.strftime("%Y-%m-%d %H:%M:%S")
+        else:
+            return None
+
+    def formatDate(self,dateobj):
+        if isinstance(dateobj, date):
+            return dateobj.strftime("%d/%m/%Y")
+        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.sirp sense consists of a list of
+        CSS/JavaScript resources defined in the
+        :mod:`waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/pages.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/pages.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/pages.py	(revision 7760)
@@ -0,0 +1,1911 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 SIRP objects.
+"""
+import copy
+import csv
+import grok
+import os
+import re
+import sys
+import time
+import re
+from zope import schema
+from zope.authentication.interfaces import (
+    IAuthentication, IUnauthenticatedPrincipal, ILogout)
+from zope.catalog.interfaces import ICatalog
+from zope.securitypolicy.interfaces import (
+    IPrincipalRoleManager, IPrincipalRoleMap)
+from zope.component import (
+    getUtility, queryUtility, createObject, getAllUtilitiesRegisteredFor)
+#from zope.component.interfaces import Invalid
+from zope.event import notify
+from zope.session.interfaces import ISession
+from waeup.sirp.browser import (
+    SIRPPage, SIRPForm, SIRPEditFormPage, SIRPAddFormPage,
+    SIRPDisplayFormPage, NullValidator)
+from waeup.sirp.browser.interfaces import (
+    IUniversity, IFacultiesContainer, IFaculty, IFacultyAdd,
+    IDepartment, IDepartmentAdd, ICourse, ICourseAdd, ICertificate,
+    ICertificateAdd, ICertificateCourse, ICertificateCourseAdd)
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.browser.resources import warning, datepicker, tabs, datatable
+from waeup.sirp.interfaces import(
+    ISIRPObject, IUsersContainer, IUserAccount, IDataCenter,
+    ISIRPXMLImporter, ISIRPXMLExporter, IBatchProcessor,
+    ILocalRolesAssignable, DuplicationError, IConfigurationContainer,
+    ISessionConfiguration, ISessionConfigurationAdd,
+    IPasswordValidator, IContactForm, ISIRPUtils)
+from waeup.sirp.permissions import get_users_with_local_roles, get_all_roles
+from waeup.sirp.students.catalog import search as searchstudents
+from waeup.sirp.university.catalog import search
+from waeup.sirp.university.vocabularies import course_levels
+from waeup.sirp.authentication import LocalRoleSetEvent
+from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
+from waeup.sirp.authentication import get_principal_role_manager
+from waeup.sirp.utils.helpers import get_user_account
+from waeup.sirp.browser.layout import jsaction, action, UtilityView
+
+grok.context(ISIRPObject)
+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))
+    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)}))
+    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 form.has_key('val_id'):
+        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)}))
+    if tab:
+        view.redirect(view.url(view.context, redirect)+'?tab%s' % tab)
+    else:
+        view.redirect(view.url(view.context, redirect))
+    return
+
+#
+# Login/logout and language switch pages...
+#
+
+class LoginPage(SIRPPage):
+    """A login page, available for all objects.
+    """
+    grok.name('login')
+    grok.context(ISIRPObject)
+    grok.require('waeup.Public')
+    label = _(u'Login')
+    camefrom = None
+    login_button = label
+
+    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':
+                    rel_link = '/students/%s' % self.request.principal.id
+                    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
+            self.flash(_('You entered wrong credentials.'))
+
+
+class LogoutPage(SIRPPage):
+    """A logout page. Calling this page will log the current user out.
+    """
+    grok.context(ISIRPObject)
+    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 SIRP!"))
+        self.redirect(self.application_url())
+
+
+class LanguageChangePage(SIRPPage):
+    """ Language switch
+    """
+    grok.context(ISIRPObject)
+    grok.name('change_language')
+    grok.require('waeup.Public')
+
+    def update(self, lang='en', view_name='@@index'):
+        self.response.setCookie('sirp.language', lang, path='/')
+        self.redirect(self.url(self.context, view_name))
+        return
+
+    def render(self):
+        return
+
+#
+# Contact form...
+#
+
+class ContactAdminForm(SIRPForm):
+    grok.name('contactadmin')
+    #grok.context(IUniversity)
+    grok.template('contactform')
+    grok.require('waeup.Authenticated')
+    pnav = 2
+    form_fields = grok.AutoFields(IContactForm).select('body')
+
+    @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()
+        sirp_utils = getUtility(ISIRPUtils)
+        success = sirp_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):
+    grok.name('enquiries')
+    grok.require('waeup.Public')
+    pnav = 2
+    form_fields = grok.AutoFields(IContactForm).select(
+                          'fullname', 'email_from', 'body')
+
+    @action(_('Send now'), style='primary')
+    def send(self, *args, **data):
+        sirp_utils = getUtility(ISIRPUtils)
+        success = sirp_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(SIRPDisplayFormPage):
+    """ The main university page.
+    """
+    grok.require('waeup.Public')
+    grok.name('index')
+    grok.context(IUniversity)
+    pnav = 0
+    label = ''
+
+    @property
+    def frontpage(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        lang = self.request.cookies.get('sirp.language', portal_language)
+        html = self.context['configuration'].frontpage_dict.get(lang,'')
+        if html =='':
+            html = self.context[
+                'configuration'].frontpage_dict.get(portal_language,'')
+        if html =='':
+            return _(u'<h1>Welcome to WAeUP.SIRP</h1>')
+        else:
+            return html
+
+class AdministrationPage(SIRPPage):
+    """ The administration overview page.
+    """
+    grok.name('administration')
+    grok.context(IUniversity)
+    grok.require('waeup.manageUniversity')
+    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.sirp 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 ()
+
+class ReindexPage(UtilityView, grok.View):
+    """ Reindex view.
+
+    Reindexes a catalog. For managers only.
+    """
+    grok.context(IUniversity)
+    grok.name('reindex')
+    grok.require('waeup.manageUniversity')
+
+    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
+
+#
+# User container pages...
+#
+
+class UsersContainerPage(SIRPPage):
+    """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):
+        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}))
+
+    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 = dict(get_all_roles())[local_role].title
+            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(SIRPAddFormPage):
+    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}))
+        except KeyError:
+            self.status = self.flash('The userid chosen already exists '
+                                  'in the database.')
+            return
+        self.redirect(self.url(self.context))
+
+class UserManageFormPage(SIRPEditFormPage):
+    """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
+
+    @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:
+            self.context.__parent__.logger.info(
+                'User account %s edited: %s' % (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()
+        sirp_utils = getUtility(ISIRPUtils)
+        success = sirp_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(SIRPPage):
+    """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(SIRPPage):
+    """General search page for the academics section.
+    """
+    grok.context(IFacultiesContainer)
+    grok.name('search')
+    grok.template('searchpage')
+    grok.require('waeup.manageUniversity')
+    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(SIRPDisplayFormPage):
+    """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 = ReSTDisplayWidget
+
+class ConfigurationContainerManageFormPage(SIRPEditFormPage):
+    """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):
+        self.applyData(self.context, **data)
+        self.context.frontpage_dict = self._frontpage()
+        self.flash(_('Settings have been saved.'))
+        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(SIRPAddFormPage):
+    """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)
+        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(SIRPEditFormPage):
+    """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):
+        self.applyData(self.context, **data)
+        self.flash(_('Settings have been saved.'))
+        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(SIRPPage):
+    grok.context(IDataCenter)
+    grok.name('index')
+    grok.require('waeup.manageUniversity')
+    label = _(u'Data Center')
+    pnav = 0
+
+class DatacenterUploadPage(SIRPPage):
+    grok.context(IDataCenter)
+    grok.name('upload')
+    grok.require('waeup.manageUniversity')
+    label = _(u'Upload file')
+    pnav = 0
+    upload_button =_(u'Upload')
+    cancel_button =_(u'Cancel')
+
+    def update(self, uploadfile=None, CANCEL=None, SUBMIT=None):
+        if CANCEL is not None:
+            self.redirect(self.url(self.context))
+            return
+        if not uploadfile:
+            return
+        try:
+            filename = uploadfile.filename
+            target = os.path.join(self.context.storage,
+                                  self.getNormalizedFileName(filename))
+            open(target, 'wb').write(uploadfile.read())
+            logger = self.context.logger
+            logger.info('%s: Uploaded file "%s"' % (
+                self.request.principal.id, target))
+        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.
+        """
+        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())
+
+class DatacenterImportStep1(SIRPPage):
+    """Manual import step 1: choose file
+    """
+    grok.context(IDataCenter)
+    grok.name('import1')
+    grok.template('datacenterimport1page')
+    grok.require('waeup.manageUniversity')
+    label = _(u'Process CSV file')
+    pnav = 0
+    cancel_button =_(u'Cancel')
+
+    def getFiles(self):
+        files = self.context.getFiles(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.sirp']
+            session['import_filename'] = select
+            self.redirect(self.url(self.context, '@@import2'))
+
+class DatacenterImportStep2(SIRPPage):
+    """Manual import step 2: choose importer
+    """
+    grok.context(IDataCenter)
+    grok.name('import2')
+    grok.template('datacenterimport2page')
+    grok.require('waeup.manageUniversity')
+    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 getPreviewBody(self):
+        """Get the first 5 rows of attached CSV file.
+        """
+        result = []
+        num = 0
+        if not self.reader:
+            return
+        for row in self.reader:
+            if num > 4:
+                break
+            num += 1
+            row = row.items()
+            # Sort fields in headerfield order
+            row = sorted(row,
+                         key=lambda k: self.reader.fieldnames.index(k[0]))
+            row = [x[1] for x in row]
+            result.append(row)
+        result.append(len(result[0]) * ['...'])
+        return result
+
+    def getImporters(self):
+        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
+        importers = [
+            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.sirp']
+        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(SIRPPage):
+    """Manual import step 3: modify header
+    """
+    grok.context(IDataCenter)
+    grok.name('import3')
+    grok.template('datacenterimport3page')
+    grok.require('waeup.manageUniversity')
+    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 getPreviewBody(self):
+        """Get the first 5 rows of attached CSV file.
+        """
+        result = []
+        num = 0
+        for row in self.reader:
+            if num > 4:
+                break
+            num += 1
+            row = row.items()
+            # Sort fields in headerfield order
+            row = sorted(row,
+                         key=lambda k: self.reader.fieldnames.index(k[0]))
+            row = [x[1] for x in row]
+            result.append(row)
+        result.append(len(result[0]) * ['...'])
+        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
+
+    @property
+    def nextstep(self):
+        return self.url(self.context, '@@import4')
+
+    def update(self, headerfield=None, back2=None, cancel=None, proceed=None):
+        session = ISession(self.request)['waeup.sirp']
+        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(SIRPPage):
+    """Manual import step 4: do actual import
+    """
+    grok.context(IDataCenter)
+    grok.name('import4')
+    grok.template('datacenterimport4page')
+    grok.require('waeup.manageUniversity')
+    label = _(u'Process CSV file')
+    pnav = 0
+    show_button =_(u'View processing log')
+    back_button =_(u'Back to data center')
+
+    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))
+            return
+        session = ISession(self.request)['waeup.sirp']
+        self.filename = session.get('import_filename', None)
+        self.mode = session.get('import_mode', None)
+        self.importername = session.get('import_importer', None)
+        self.headerfields = session.get('import_headerfields', None)
+
+        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(SIRPPage):
+    grok.context(IDataCenter)
+    grok.name('logs')
+    grok.template('datacenterlogspage')
+    grok.require('waeup.manageUniversity')
+    label = _(u'Show logfiles')
+    pnav = 0
+    back_button = _(u'Back to Data Center')
+    show_button = _(u'Show')
+
+    def update(self, show=None, logname=None, back=None):
+        session = ISession(self.request)['waeup.sirp']
+        if back is not None:
+            self.redirect(self.url(self.context))
+            return
+        if logname is not None:
+            session['logname'] = logname
+        if show is not None:
+            self.redirect(self.url(self.context, '@@show'))
+            return
+        self.files = self.context.getLogFiles()
+
+class DatacenterLogsFileview(SIRPPage):
+    grok.context(IDataCenter)
+    grok.name('show')
+    grok.template('datacenterlogsshowfilepage')
+    grok.require('waeup.manageUniversity')
+    title = _(u'Data Center')
+    pnav = 0
+    search_button = _('Search')
+    back_button = _('Back')
+
+    def label(self):
+        return "Logfile %s" % self.filename
+
+    def update(self, back=None, query=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
+        session = ISession(self.request)['waeup.sirp']
+        logname = session.get('logname', None)
+        if back is not None or logname is None:
+            self.redirect(self.url(self.context, '@@logs'))
+            return
+        self.filename = logname
+        form = self.request.form
+        self.query = ''
+        query = form.get('query')
+        if search is None or not query:
+            return
+        self.files = self.context.getLogFiles()
+        fullpath = os.path.join(self.context.storage, 'logs', logname)
+        self.query = query
+        #self.filecontents = open(fullpath, 'rb').read()
+        popen = os.popen('grep %s %s' % (query,fullpath))
+        self.grepresult = popen.read()
+        popen.close()
+
+class DatacenterSettings(SIRPPage):
+    grok.context(IDataCenter)
+    grok.name('manage')
+    grok.template('datacentermanagepage')
+    grok.require('waeup.manageUniversity')
+    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.'))
+            self.redirect(self.url(self.context))
+        return
+
+class ExportXMLPage(grok.View):
+    """Deliver an XML representation of the context.
+    """
+    grok.name('export.xml')
+    grok.require('waeup.manageUniversity')
+
+    def render(self):
+        exporter = ISIRPXMLExporter(self.context)
+        xml = exporter.export().read()
+        self.response.setHeader(
+            'Content-Type', 'text/xml; charset=UTF-8')
+        return xml
+
+class ImportXMLPage(SIRPPage):
+    """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.manageUniversity')
+
+    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 = ISIRPXMLImporter(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(SIRPPage):
+    """ 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(SIRPEditFormPage):
+    """Manage the basic properties of a `Faculty` instance.
+    """
+    grok.context(IFacultiesContainer)
+    grok.name('manage')
+    grok.require('waeup.manageUniversity')
+    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):
+        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(SIRPAddFormPage):
+    """ Page form to add a new faculty to a faculty container.
+    """
+    grok.context(IFacultiesContainer)
+    grok.require('waeup.manageUniversity')
+    grok.name('add')
+    label = _('Add faculty')
+    form_fields = grok.AutoFields(IFacultyAdd)
+    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
+        self.redirect(self.url(self.context, u'@@manage')+'?tab1')
+
+    @action(_('Cancel'))
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+#
+# Faculty pages
+#
+class FacultyPage(SIRPPage):
+    """Index page of faculties.
+    """
+    grok.context(IFaculty)
+    grok.require('waeup.viewAcademics')
+    grok.name('index')
+    pnav = 1
+
+    @property
+    def label(self):
+        return _('Departments')
+
+class FacultyManageFormPage(SIRPEditFormPage):
+    """Manage the basic properties of a `Faculty` instance.
+    """
+    grok.context(IFaculty)
+    grok.name('manage')
+    grok.require('waeup.manageUniversity')
+    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)
+
+    @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()
+
+    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 delDepartments(self, **data):
+        delSubobjects(self, redirect='@@manage', tab='2')
+        return
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        self.flash(_('Form has been saved.'))
+        return
+
+    @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 DepartmentAddFormPage(SIRPAddFormPage):
+    """Add a department to a faculty.
+    """
+    grok.context(IFaculty)
+    grok.name('add')
+    grok.require('waeup.manageUniversity')
+    label = _('Add department')
+    form_fields = grok.AutoFields(IDepartmentAdd)
+    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']}))
+        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
+
+    @action(_('Cancel'))
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+#
+# Department pages
+#
+class DepartmentPage(SIRPPage):
+    """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 ShowStudentsInDepartmentPage(SIRPPage):
+    """Page that lists all students in the department.
+    """
+    grok.context(IDepartment)
+    grok.require('waeup.showStudents')
+    grok.name('showdepstudents')
+    grok.template('showstudentspage')
+    pnav = 1
+    label = _('Students')
+
+    @property
+    def getStudents(self):
+        hitlist = searchstudents(query=self.context.code,
+            searchtype='depcode', view=self)
+        return hitlist
+
+    def update(self, *args, **kw):
+        datatable.need()
+        return
+
+class ShowStudentsInCertificatePage(ShowStudentsInDepartmentPage):
+    """Page that lists all students studying a certificate.
+    """
+    grok.context(ICertificate)
+    grok.require('waeup.showStudents')
+    grok.name('showcertstudents')
+    pnav = 1
+    label = _('Students')
+
+    @property
+    def getStudents(self):
+        hitlist = searchstudents(query=self.context.code,
+            searchtype='certcode', view=self)
+        return hitlist
+
+class DepartmentManageFormPage(SIRPEditFormPage):
+    """Manage the basic properties of a `Department` instance.
+    """
+    grok.context(IDepartment)
+    grok.name('manage')
+    grok.require('waeup.manageUniversity')
+    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)
+
+    @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
+
+    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)
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        self.flash('Form has been saved.')
+        return
+
+    @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):
+        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(SIRPAddFormPage):
+    """Add-form to add course to a department.
+    """
+    grok.context(IDepartment)
+    grok.name('addcourse')
+    grok.require('waeup.manageUniversity')
+    label = _(u'Add course')
+    form_fields = grok.AutoFields(ICourseAdd)
+    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)
+        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+class CertificateAddFormPage(SIRPAddFormPage):
+    """Add-form to add certificate to a department.
+    """
+    grok.context(IDepartment)
+    grok.name('addcertificate')
+    grok.require('waeup.manageUniversity')
+    label = _(u'Add certificate')
+    form_fields = grok.AutoFields(ICertificateAdd)
+    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)
+        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(SIRPDisplayFormPage):
+    """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(SIRPEditFormPage):
+    """Edit form page for courses.
+    """
+    grok.context(ICourse)
+    grok.name('manage')
+    grok.require('waeup.manageUniversity')
+    label = _(u'Edit course')
+    pnav = 1
+
+    form_fields = grok.AutoFields(ICourse)
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        self.flash(_('Form has been saved.'))
+        return
+
+    #@action('Save and return')
+    #def saveAndReturn(self, **data):
+    #    self.applyData(self.context, **data)
+    #    self.redirect(self.url(self.context))
+    #    return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+#
+# Certificate pages
+#
+class CertificatePage(SIRPDisplayFormPage):
+    """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(SIRPEditFormPage):
+    """Manage the properties of a `Certificate` instance.
+    """
+    grok.context(ICertificate)
+    grok.name('manage')
+    grok.require('waeup.manageUniversity')
+    pnav = 1
+    label = _('Edit certificate')
+
+    form_fields = grok.AutoFields(ICertificate)
+
+    pnav = 1
+    grok.template('certificatemanagepage')
+    taboneactions = [_('Save'),_('Cancel')]
+    tabtwoactions = [_('Add course referrer'),
+                     _('Remove selected course referrers'),_('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):
+        self.applyData(self.context, **data)
+        self.flash(_('Form has been saved.'))
+        return
+
+    @jsaction(_('Remove selected course referrers'))
+    def delCertificateCourses(self, **data):
+        delSubobjects(self, redirect='@@manage', tab='2')
+        return
+
+    @action(_('Add course referrer'), 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
+
+    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)
+
+    @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(SIRPAddFormPage):
+    """Add-page to add a course ref to a certificate
+    """
+    grok.context(ICertificate)
+    grok.name('addcertificatecourse')
+    grok.require('waeup.manageUniversity')
+    form_fields = grok.AutoFields(ICertificateCourseAdd)
+    pnav = 1
+    label = _('Add course referrer')
+
+    @action(_('Add course referrer'))
+    def addCertcourse(self, **data):
+        try:
+            self.context.addCourseRef(**data)
+        except KeyError:
+            self.status = self.flash(_('The chosen course referrer is already '
+                                  'part of this certificate.'))
+            return
+        self.status = self.flash(
+            _("Course referrer ${a}_${b} added.",
+            mapping = {'a': data['course'].code, 'b': data['level']}))
+        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+
+#
+# Certificate course pages...
+#
+class CertificateCoursePage(SIRPPage):
+    """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(SIRPEditFormPage):
+    """Manage the basic properties of a `CertificateCourse` instance.
+    """
+    grok.context(ICertificateCourse)
+    grok.name('manage')
+    grok.require('waeup.manageUniversity')
+    form_fields = grok.AutoFields(ICertificateCourse)
+    label = _('Edit course referrer')
+    pnav = 1
+
+    @action(_('Save and return'), style='primary')
+    def saveAndReturn(self, **data):
+        parent = self.context.__parent__
+        if self.context.level == data['level']:
+            self.applyData(self.context, **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.addCourseRef(**data)
+            parent.delCourseRef(data['course'].code, level=old_level)
+        self.flash(_('Form has been saved.'))
+        self.redirect(self.url(parent))
+        return
+
+    @action(_('Cancel'), validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/resources.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/resources.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/resources.py	(revision 7760)
@@ -0,0 +1,273 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.
+"""
+#from hurry import yui
+from hurry.resource import Library, ResourceInclusion
+from hurry.jqueryui import jqueryui, base, blitzer, ui_lightness, humanity
+from hurry.jquery import jquery
+
+
+#: All local resources are registered under the name ``waeup_sirp``.
+waeup_sirp = Library('waeup_sirp', 'static')
+
+#: 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.sirp.browser.resources import datepicker
+#:     # ...
+#:     class MyPage(...):
+#:       # ...
+#:       def update(self):
+#:         datepicker.need()
+#:
+#:   This way all required JavaScripts 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_sirp, '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.sirp.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_sirp, '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.sirp.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_sirp, '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.sirp.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_sirp, '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_sirp, 'jquery.dataTables.js',
+                               minified='jquery.dataTables.min.js',
+                               depends=[jquery])
+
+#: A stylesheet for datatables
+datatables_css = ResourceInclusion(waeup_sirp, '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.sirp.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_sirp, 'datatable.js',
+                              depends=[datatables, datatables_css])
+
+#: Register Twitter's Bootsrap css including dropdown js as a resource.
+bootstrap_min = ResourceInclusion(
+    waeup_sirp, 'bootstrap-1.4.0.css')
+
+dropdown = ResourceInclusion(
+    waeup_sirp, 'bootstrap-dropdown-1.4.0.js',
+    depends=[jquery])
+
+#: Register basic SIRP CSS (which is based on ``bootstrap.min-1.4.0.css`` 
+#: and ``base`` as a resource.
+waeup_base_css = ResourceInclusion(
+    waeup_sirp, 'waeup-base.css',
+    depends=[dropdown,bootstrap_min, base])
+
+#: A basic theme based on jQuery only (crappy yet).
+waeuptheme_empty = ResourceInclusion(
+    waeup_sirp, 'empty.css',
+    depends=[humanity])
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/bootstrap-1.4.0.css
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/bootstrap-1.4.0.css	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/bootstrap-1.4.0.css	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/bootstrap-dropdown-1.4.0.js
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/bootstrap-dropdown-1.4.0.js	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/bootstrap-dropdown-1.4.0.js	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/bootstrap-tabs-1.4.0.js
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/bootstrap-tabs-1.4.0.js	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/bootstrap-tabs-1.4.0.js	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/datatable.js
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/datatable.js	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/datatable.js	(revision 7760)
@@ -0,0 +1,8 @@
+$(document).ready(function(){
+    $('.dataTable').dataTable();
+	$('.dataTableManage').dataTable( {
+		"aaSorting": [[ 1, "asc" ]],
+		"aoColumnDefs":[{ "bSortable": false, "aTargets": [ 0 ] }]
+	} );    
+});
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/datatables.css
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/datatables.css	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/datatables.css	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/datepicker.js
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/datepicker.js	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/datepicker.js	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/empty.css
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/empty.css	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/empty.css	(revision 7760)
@@ -0,0 +1,1 @@
+/* Nothing here */
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/jquery.dataTables.js
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/jquery.dataTables.js	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/jquery.dataTables.js	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/jquery.dataTables.min.js
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/jquery.dataTables.min.js	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/jquery.dataTables.min.js	(revision 7760)
@@ -0,0 +1,145 @@
+/*
+ * 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.
+ */
+(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=10;this._iDisplayStart=0;this._iDisplayEnd=10;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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/mobile.css
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/mobile.css	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/mobile.css	(revision 7760)
@@ -0,0 +1,35 @@
+.container {
+  width: 300px;
+}
+
+body {
+  padding-top: 100px;
+}
+
+select {
+  width: 50px;
+}
+
+.dataTables_length {
+  float: left;
+  width: 50%;
+}
+
+.dataTables_filter {
+  float: right;
+  text-align: right;
+  width: 50%;
+}
+
+.topbar h3 a, .topbar .brand  {
+  margin-left: -25px;
+  padding-bottom: 5px;
+  padding-left: 20px;
+  padding-right: 20px;
+  padding-top: 2px;
+}
+
+input, textarea,
+textarea.span9 {
+  width: 70%;
+}
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/tabs.js
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/tabs.js	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/tabs.js	(revision 7760)
@@ -0,0 +1,4 @@
+/* See http://docs.jquery.com/UI/Tabs */
+$(document).ready(function() {
+    $("#tabs").tabs();
+  });
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/toggleall.js
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/toggleall.js	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/toggleall.js	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/waeup-base.css
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/waeup-base.css	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/waeup-base.css	(revision 7760)
@@ -0,0 +1,224 @@
+/* This is the base stylesheet for SIRP. 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;
+}
+
+.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: 700px;
+}
+
+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: 180px;
+}
+
+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;
+}
+
+.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;
+}
+
+ /* SIRP stuff */
+
+div.actionbar {
+  margin-top: -24px;
+  margin-left: -15px;
+  margin-bottom: 6px;
+}
+
+a.actionbar {
+  margin-bottom: 6px;
+}
+
+.error {
+  color: red;
+}
+
+.form-table {
+  width: 100%;
+}
+
+.form-table td {
+  padding: 5px 5px 4px;
+  border-top: none;
+}
+
+.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: 700px;
+  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;
+}
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/warning.js
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/warning.js	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/static/warning.js	(revision 7760)
@@ -0,0 +1,7 @@
+function confirmPost(question) {
+    var agree=confirm(question);
+    if (agree)
+    return true ;
+    else
+    return false ;
+};
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/actionbutton.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/actionbutton.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/actionbutton.pt	(revision 7760)
@@ -0,0 +1,7 @@
+<a class="btn actionbar" tal:condition="viewlet/target_url"
+   tal:attributes="href viewlet/target_url">
+  <img src="" alt="edit"
+   tal:attributes="src viewlet/icon_url;
+	   alt viewlet/alt"/>
+  <span tal:replace="viewlet/text">TEXT</span>
+</a>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/addaction.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/addaction.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/addaction.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/administrationpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/administrationpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/administrationpage.pt	(revision 7760)
@@ -0,0 +1,5 @@
+<p>
+  You can manage the following tasks:
+</p>
+
+<tal:admintasks tal:content="structure provider:admintasks" />
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/admintask.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/admintask.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/admintask.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/breadcrumbs.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/breadcrumbs.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/breadcrumbs.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/certificatecoursepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/certificatecoursepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/certificatecoursepage.pt	(revision 7760)
@@ -0,0 +1,37 @@
+<table i18n:domain="waeup.sirp" 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="">Is mandatory course (not elective):</td>
+      <td tal:content="context/mandatory">MANDATORY</td>
+    </tr>
+  </tbody>
+</table>
+
+
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/certificatemanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/certificatemanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/certificatemanagepage.pt	(revision 7760)
@@ -0,0 +1,141 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      enctype="multipart/form-data" i18n:domain="waeup.sirp">
+
+  <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="">Course Referrers</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 i18n:translate="">Course Referrers</h3>
+      <table class="display dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;</th><th i18n:translate="">Level</th>
+            <th i18n:translate="">Semester</th><th i18n:translate="">Referrer</th>
+            <th i18n:translate="">Course</th><th i18n:translate="">Title</th>
+            <th i18n:translate="">Mandatory</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">
+        	   MANDATORY
+              </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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/certificatepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/certificatepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/certificatepage.pt	(revision 7760)
@@ -0,0 +1,65 @@
+<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.sirp" i18n:translate="">Course Referrers</span>
+</h3>
+<br />
+<table i18n:domain="waeup.sirp" class="display dataTable">
+<thead>
+  <tr>
+    <th i18n:translate="">Level</th><th i18n:translate="">Semester</th>
+    <th i18n:translate="">Referrer</th><th i18n:translate="">Course</th>
+    <th i18n:translate="">Title</th><th i18n:translate="">Mandatory</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">
+	   MANDATORY
+      </span>
+    </td>                
+  </tr>
+</tbody>
+</table>
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/configurationmanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/configurationmanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/configurationmanagepage.pt	(revision 7760)
@@ -0,0 +1,50 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+  enctype="multipart/form-data" i18n:domain="waeup.sirp">
+    <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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/contactform.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/contactform.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/contactform.pt	(revision 7760)
@@ -0,0 +1,24 @@
+<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>
+    <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>
+
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterimport1page.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterimport1page.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterimport1page.pt	(revision 7760)
@@ -0,0 +1,34 @@
+<h3 i18n:domain="waeup.sirp" i18n:translate="">Step 1</h3>
+<p i18n:domain="waeup.sirp" 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.sirp" i18n:translate="">
+  Please select a file for processing from the list below.
+</p>
+<form i18n:domain="waeup.sirp" 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterimport2page.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterimport2page.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterimport2page.pt	(revision 7760)
@@ -0,0 +1,64 @@
+<h3 i18n:domain="waeup.sirp" i18n:translate="">Step 2</h3>
+<form i18n:domain="waeup.sirp" 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="body view/getPreviewBody;
+                    headers view/getPreviewHeader;
+                    fieldnum python: len(headers)">
+      <thead>
+        <tr>
+          <th tal:repeat="headerfield headers"
+              tal:content="headerfield"> headerfield
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="row body">
+          <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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterimport3page.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterimport3page.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterimport3page.pt	(revision 7760)
@@ -0,0 +1,83 @@
+<div i18n:domain="waeup.sirp" i18n:translate=""
+  class="alert-message success" tal:condition="not: view/getWarnings">
+  Header fields OK
+</div>
+
+<h3 i18n:domain="waeup.sirp" i18n:translate="">Step 3</h3>
+<form i18n:domain="waeup.sirp">
+  <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 class="condensed-table"
+     tal:define="body view/getPreviewBody;
+           headers view/headerfields;
+           fieldnum python: len(headers)">
+      <thead>
+        <tr>
+          <th tal:repeat="headerfield headers"
+              tal:content="headerfield">
+            headerfield
+          </th>
+        </tr>
+        <tr>
+          <th tal:repeat="headerfield headers">
+            <i i18n:translate="">change to:</i>
+          </th>
+        </tr>
+        <tr>
+          <th tal:repeat="headerfield headers">
+            <select name="headerfield">
+              <option tal:repeat="opt view/getPossibleHeaders"
+                tal:attributes="value opt/value;
+                  selected python: opt['value']==headerfield and 'selected' or '';"
+                tal:content="opt/title">
+                new_header_name
+              </option>
+            </select>
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="row body">
+          <td tal:repeat="value row"
+              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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterimport4page.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterimport4page.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterimport4page.pt	(revision 7760)
@@ -0,0 +1,21 @@
+<h3 i18n:domain="waeup.sirp" i18n:translate="">Step 4</h3>
+<p i18n:domain="waeup.sirp" i18n:translate="">
+  Batch processing finished.
+</p>
+<p i18n:domain="waeup.sirp">
+  <b i18n:translate="">File:</b> <span tal:content="view/filename">Filename.csv</span>
+</p>
+<p i18n:domain="waeup.sirp">
+  <b i18n:translate="">Processor:</b>
+  <span tal:content="view/importer/name">Importer Name</span>
+</p>
+<p i18n:domain="waeup.sirp">
+  <b i18n:translate="">Processing mode: </b>
+  <span tal:content="view/mode">mode</span>
+</p>
+<form method="POST" i18n:domain="waeup.sirp">
+  <input class="btn primary" type="submit" name="finish"
+    tal:attributes="value view/back_button" />
+  <input class="btn" type="submit" name="showlog"
+    tal:attributes="value view/show_button" />
+</form>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterlogspage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterlogspage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterlogspage.pt	(revision 7760)
@@ -0,0 +1,37 @@
+<div i18n:domain="waeup.sirp" tal:define="files view/files">
+  <p i18n:translate="" tal:condition="not: files">
+  Currently no log files are available.
+  </p>
+  <table tal:condition="files" class="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">
+            <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">
+    <input i18n:translate="" type="submit" class="btn" name="back"
+        tal:attributes="value view/back_button"/>
+  </form>
+</div>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterlogsshowfilepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterlogsshowfilepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterlogsshowfilepage.pt	(revision 7760)
@@ -0,0 +1,14 @@
+<form method="post">
+  <input type="text" name="query"
+	 tal:attributes="value view/query" />
+    <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"/>
+</form>
+
+<div tal:condition="view/grepresult|nothing">
+  <pre tal:content="structure view/grepresult">
+    GREPRESULT
+  </pre>
+</div>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacentermanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacentermanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacentermanagepage.pt	(revision 7760)
@@ -0,0 +1,24 @@
+<form  i18n:domain="waeup.sirp" 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenterpage.pt	(revision 7760)
@@ -0,0 +1,26 @@
+<p  i18n:domain="waeup.sirp" 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.sirp">
+  <b i18n:translate="">Storage path:</b>
+  <span tal:content="context/storage">/foo/bar</span>
+</p>
+
+<table i18n:domain="waeup.sirp">
+  <thead>
+    <tr>
+      <th i18n:translate="">File</th>
+      <th i18n:translate="">Size</th>
+      <th i18n:translate="">Upload Date</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="file context/getFiles">
+      <td tal:content="file/name">FILENAME</td>
+      <td tal:content="file/size">12 k</td>
+      <td tal:content="file/uploaddate">Mar 12, 2008</td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenteruploadpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenteruploadpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/datacenteruploadpage.pt	(revision 7760)
@@ -0,0 +1,10 @@
+<form method="post" enctype="multipart/form-data">
+  <input type="file" name="uploadfile:file" />
+  <br /><br />
+  <div>
+    <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"/>
+  </div>
+</form>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/default_waeup_display_form.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/default_waeup_display_form.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/default_waeup_display_form.pt	(revision 7760)
@@ -0,0 +1,15 @@
+<table 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>
+    <tal:files content="structure provider:files" />
+  </tbody>
+</table>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/default_waeup_edit_form.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/default_waeup_edit_form.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/default_waeup_edit_form.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/departmentmanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/departmentmanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/departmentmanagepage.pt	(revision 7760)
@@ -0,0 +1,146 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+  i18n:domain="waeup.sirp" 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/departmentpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/departmentpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/departmentpage.pt	(revision 7760)
@@ -0,0 +1,57 @@
+<ul i18n:domain="waeup.sirp" 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.sirp" 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>
+    <br /><br />
+    </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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/exception.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/exception.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/exception.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/exportxmlaction.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/exportxmlaction.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/exportxmlaction.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/facultiescontainermanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/facultiescontainermanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/facultiescontainermanagepage.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/facultymanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/facultymanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/facultymanagepage.pt	(revision 7760)
@@ -0,0 +1,120 @@
+<form action="." tal:attributes="action request/URL"
+  i18n:domain="waeup.sirp" 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/facultypage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/facultypage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/facultypage.pt	(revision 7760)
@@ -0,0 +1,19 @@
+<div i18n:domain="waeup.sirp" i18n:translate="" tal:condition="python: not len(context.keys())">
+There no subobjects registered yet.
+</div>
+
+<table i18n:domain="waeup.sirp">
+  <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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/importxmlaction.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/importxmlaction.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/importxmlaction.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/importxmlpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/importxmlpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/importxmlpage.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/loginpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/loginpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/loginpage.pt	(revision 7760)
@@ -0,0 +1,47 @@
+<form i18n:domain="waeup.sirp" 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">
+    You don't have an account because you are a fresh student, or your student record has just been created?
+    Acquire a Password Access Code (PWD) and inititialize your student account
+    <strong><a href  ="setpassword"> here</a></strong>.
+  </p>
+  <p i18n:translate="login_trouble3"> Or simply forgot your student id or password? Then request a new password
+    <strong><a href="changepw"> here</a></strong>.
+  </p>
+</form>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/logoutpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/logoutpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/logoutpage.pt	(revision 7760)
@@ -0,0 +1,2 @@
+<div>
+</div>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/messages.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/messages.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/messages.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/myrolespage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/myrolespage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/myrolespage.pt	(revision 7760)
@@ -0,0 +1,29 @@
+<h3 i18n:domain="waeup.sirp"
+    i18n:translate="">My Portal Roles:</h3>
+<table i18n:domain="waeup.sirp" class="form-table">
+<tr tal:repeat="role view/getSiteRoles">
+    <td>
+      <div tal:content="role" >ROLE</div>
+    </td>
+  </tr>
+</table>
+
+<h3 i18n:domain="waeup.sirp"
+    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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/notfound.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/notfound.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/notfound.pt	(revision 7760)
@@ -0,0 +1,16 @@
+<h2 i18n:domain="waeup.sirp" i18n:translate="">
+  The page you are trying to access is not available.
+</h2>
+
+<div i18n:domain="waeup.sirp">
+  <b i18n:translate="">Please note the following:</b></div>
+<div i18n:domain="waeup.sirp">
+  <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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/plainactionbutton.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/plainactionbutton.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/plainactionbutton.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/primarynavtab.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/primarynavtab.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/primarynavtab.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/searchpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/searchpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/searchpage.pt	(revision 7760)
@@ -0,0 +1,33 @@
+<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.sirp" 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="">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/type">
+                TTYPE
+            </td>
+            </tr>
+        </tbody>
+    </table>
+</span>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/showstudentspage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/showstudentspage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/showstudentspage.pt	(revision 7760)
@@ -0,0 +1,30 @@
+<table i18n:domain="waeup.sirp" class="display dataTable">
+  <thead>
+	<tr>
+	  <th i18n:translate="">Student Id</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>
+	</tr>
+  </thead>
+  <tbody>
+	<tr tal:repeat="item view/getStudents" 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/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>
+	</tr>
+  </tbody>
+</table>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/staffsitelayout.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/staffsitelayout.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/staffsitelayout.pt	(revision 7760)
@@ -0,0 +1,122 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" i18n:domain="waeup.sirp">
+  <head>
+    <title>WAeUP - your way up
+    </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="shortcut icon" tal:attributes="href static/favicon.ico" type="image/x-icon" />
+    <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">
+            <tal:primary_nav content="structure provider:primary_nav" />
+          </ul>
+          <ul class="nav secondary-nav">
+            <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:languages
+                  tal:content="structure provider:languages" /> |
+          </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.getStudent().student_id">
+            ID
+          </span>&nbsp;&nbsp;
+          <span i18n:translate="">State:</span>
+          <span tal:replace="python:context.getStudent().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" />
+            </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 i18n:translate="">
+            Form Status:
+          </span>
+          <span tal:content="view/status">
+            FORM STATUS SUMMARY
+          </span>
+        </div>
+        <h1 tal:condition="view/label|nothing"
+            tal:content="view/label">LABEL</h1>
+        <div tal:content="structure view/content"> THE CONTENT
+        </div>
+        <div class="footer">
+          <p class="pull-right"><a i18n:translate="" href="#">Back to top</a></p>
+          <p i18n:translate="">
+            Student Information and Registration Portal<br />
+            Copyright &copy; WAeUP Group, 2008-2012
+          </p>
+        </div>
+      </div>
+    </div>
+  </body>
+</html>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/studentsitelayout.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/studentsitelayout.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/studentsitelayout.pt	(revision 7760)
@@ -0,0 +1,108 @@
+<!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.sirp">
+  <head>
+    <title>WAeUP - your way up
+    </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" />
+    <meta name="viewport" content="width=320, initial-scale=1, maximum-scale=1"/>
+    <link rel="stylesheet" media="only screen and (max-device-width: 480px)"
+        tal:attributes="href python: view.url(layout.site, '@@/waeup.sirp.browser/mobile.css')"
+        type="text/css" />
+    <link rel="shortcut icon" tal:attributes="href static/favicon.ico" type="image/x-icon" />
+    <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">
+              <tal:primary_nav content="structure provider:primary_nav" />
+              <tal:primary_nav content="structure provider:primary_nav_student" />
+          </ul>
+          <ul class="nav secondary-nav">
+            <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:languages
+                  tal:content="structure provider:languages" /> |
+          </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.getStudent().student_id">
+              Id
+            </span>
+            |
+            <span tal:replace="python:context.getStudent().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 i18n:translate="">
+            Form Status:
+          </span>
+          <span tal:content="view/status">
+            FORM STATUS SUMMARY
+          </span>
+        </div>
+        <h1 tal:condition="view/label|nothing"
+            tal:content="view/label">Label</h1>
+        <div tal:content="structure view/content">
+          THE CONTENT
+        </div>
+        <div class="footer">
+          <p class="pull-right"><a i18n:translate="" href="#">Back to top</a></p>
+          <p i18n:translate="">
+            Student Information and Registration Portal<br />
+            Copyright &copy; WAeUP Group, 2008-2012
+          </p>
+        </div>
+      </div>
+    </div>
+  </body>
+</html>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/subobjectlist.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/subobjectlist.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/subobjectlist.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/unauthorized.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/unauthorized.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/unauthorized.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/universitypage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/universitypage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/universitypage.pt	(revision 7760)
@@ -0,0 +1,1 @@
+<span tal:replace="structure view/frontpage">Frontpage</span>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/universityrss20feed.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/universityrss20feed.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/universityrss20feed.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/usereditformpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/usereditformpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/usereditformpage.pt	(revision 7760)
@@ -0,0 +1,27 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.sirp" enctype="multipart/form-data">
+
+  <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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/userscontainerpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/userscontainerpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/userscontainerpage.pt	(revision 7760)
@@ -0,0 +1,29 @@
+<table i18n:domain="waeup.sirp">
+  <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 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/widgets.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/widgets.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/templates/widgets.pt	(revision 7760)
@@ -0,0 +1,19 @@
+<tal:block repeat="widget view/widgets">
+  <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">
+        <span tal:replace="structure widget/error">ERROR</span>
+      </tal:error>
+      <tal:hint tal:condition="widget/hint">
+        <div class="hint" tal:content="structure widget/hint">HINT</div>
+      </tal:hint>
+    </td>
+  </tr>
+</tal:block>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/tests/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/tests/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/tests/__init__.py	(revision 7760)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/tests/test_captcha.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/tests/test_captcha.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/tests/test_captcha.py	(revision 7760)
@@ -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.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.browser.captcha import (
+    CaptchaResponse, CaptchaRequest, NullCaptcha, StaticCaptcha, ReCaptcha,
+    CaptchaManager)
+from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/tests/test_doctests.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/tests/test_doctests.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/tests/test_doctests.py	(revision 7760)
@@ -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.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/tests/test_permissions.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/tests/test_permissions.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/tests/test_permissions.py	(revision 7760)
@@ -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.sirp.app import University
+from waeup.sirp.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.addCourseRef(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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/theming.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/theming.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/theming.py	(revision 7760)
@@ -0,0 +1,115 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.interfaces import SimpleSIRPVocabulary
+from waeup.sirp.browser.interfaces import ITheme
+from waeup.sirp.browser.resources import (
+    waeuptheme_empty, waeup_base_css,
+    )
+
+class SIRPThemeBase(grok.GlobalUtility):
+    grok.implements(ITheme)
+    grok.name('base waeup theme')
+
+    description = u"Base Theme"
+
+    def getResources(self):
+        return [waeup_base_css]
+
+#class SIRPThemeRed1(grok.GlobalUtility):
+#    grok.implements(ITheme)
+#    grok.name('red waeup theme')
+
+#    description = u"Uli's Red Theme"
+
+#    def getResources(self):
+#        return [waeuptheme_red1]
+
+#class SIRPThemeGray1(grok.GlobalUtility):
+#    grok.implements(ITheme)
+#    grok.name('gray waeup theme')
+
+#    description = u"Henrik's Gray Theme"
+
+#    def getResources(self):
+#        return [waeuptheme_gray1]
+
+class SIRPThemeEmpty(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.sirp.browser.theming.ThemesVocabulary'
+
+    Interface fields that wish to provide a list of available themes
+    can require a 'named vocabulary', i.e. set:
+
+      vocabulary = 'waeup.sirp.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.sirp.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 = SimpleSIRPVocabulary(*terms)
+        return vocab
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/viewlets.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/viewlets.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/browser/viewlets.py	(revision 7760)
@@ -0,0 +1,763 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.browser.pages import (
+    UniversityPage, FacultiesContainerPage, DatacenterPage, FacultyPage,
+    DepartmentPage, CoursePage, CertificatePage, CertificateCoursePage,
+    UsersContainerPage, UserManageFormPage)
+from waeup.sirp.browser.interfaces import (
+    IFacultiesContainer, IFaculty, IDepartment, ICourse, ICertificate,
+    ICertificateCourse, IBreadcrumbContainer, IUniversity, IUsersContainer)
+from waeup.sirp.interfaces import (
+    ISIRPUtils, ISIRPObject, ISIRPXMLExporter,
+    ISIRPXMLImporter, IDataCenter, IUserAccount)
+from waeup.sirp.browser.layout import SIRPPage, default_primary_nav_template
+from waeup.sirp.utils.helpers import get_user_account
+
+from waeup.sirp.interfaces import MessageFactory as _
+
+grok.templatedir('templates')
+grok.context(ISIRPObject) # Make ISIRPObject the default context
+
+class LeftSidebar(grok.ViewletManager):
+    grok.name('left')
+
+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 and links to a
+    target.  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.
+
+    """
+    grok.baseclass()
+    grok.context(ISIRPObject)
+    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.s.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.sirp.browser/static), we set the directory
+            # resource appropiately.
+            #
+            # XXX: The hardcoding of 'w.s.browser' should be replaced
+            #      by something smarter.
+            #
+            # TODO: notes in here should go to general documentation.
+            static = queryAdapter(
+                self.request, Interface, name='waeup.sirp.browser')
+        return static[self.icon]()
+
+    @property
+    def target_url(self):
+        """Get a URL to the target...
+        """
+        return self.view.url(self.view.context, self.target)
+
+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.manageUniversity')
+    icon = 'actionicon_modify.png'
+    target = '@@manage'
+    text = _('Edit')
+
+class AddActionButton(ActionButton):
+    """A base for 'add' buttons.
+    """
+    grok.baseclass()
+    grok.order(4)
+    grok.require('waeup.manageUniversity')
+    icon = 'actionicon_add.png'
+    target = 'add'
+    text = _('Add')
+    
+class RemoveActionButton(ActionButton):
+    """A base for 'remove' buttons.
+    """
+    grok.baseclass()
+    grok.order(4)
+    grok.require('waeup.manageUniversity')
+    icon = 'actionicon_delete.png'
+    target = 'remove'
+    text = _('Remove')
+
+class SearchActionButton(ActionButton):
+    """A base for 'search' buttons.
+    """
+    grok.baseclass()
+    grok.order(5)
+    grok.require('waeup.manageUniversity')
+    icon = 'actionicon_search.png'
+    target = 'search'
+    text = _('Search')
+
+
+#
+# General viewlets (for more than one page/context)
+#
+
+class BreadCrumbs(grok.Viewlet):
+    grok.context(ISIRPObject)
+    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:
+                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(ISIRPObject)
+    grok.require('waeup.Public')
+    title = u'Languages'
+
+    def render(self):
+        preferred_languages = getUtility(ISIRPUtils).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.manageUniversity')
+
+#class ImportXMLAction(grok.Viewlet):
+#    grok.viewletmanager(ActionBar)
+#    #grok.view(Index)
+#    grok.order(99)
+#    grok.require('waeup.manageUniversity')
+#
+#    def update(self):
+#        # We cannot simply replace local sites.
+#        self.can_import = not ISite.providedBy(self.context)
+
+class LeftSidebarLink(grok.Viewlet):
+    """ An entry on left sidebar.
+
+    This is only a baseclass that won't be rendered actually. Deriving
+    viewlets can override certain values and will be rendered with the
+    values set here as default.
+    """
+    grok.baseclass()
+    grok.viewletmanager(LeftSidebar)
+    grok.context(ISIRPObject)
+    grok.order(5)
+    grok.require('waeup.manageUniversity')
+    icon = 'actionicon_modify.png' # File must exist in static/
+    title = 'Text of link'
+
+    @property
+    def url(self):
+        return '@@index'
+
+    @property
+    def icon_url(self):
+        """Get the icon URL.
+        """
+        if self.icon:
+            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.s.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.sirp.browser/static), we set the directory
+                # resource appropiately.
+                #
+                # XXX: The hardcoding of 'w.s.browser' should be replaced
+                #      by something smarter.
+                #
+                # TODO: notes in here should go to general documentation.
+                static = queryAdapter(
+                    self.request, Interface, name='waeup.sirp.browser')
+            return static[self.icon]()
+        return
+    
+    # Render link only if url is provided.
+    def render(self):
+        if self.url:
+            if self.icon_url:
+                return u'<li><a href="%s"><img src="%s" /> %s </a></li>' % (
+                    self.url, self.icon_url, self.title)
+            else:
+                return u'<li><a href="%s">%s </a></li>' % (
+                    self.url, self.title)
+        else:
+            return ''
+
+class WidgetsTableRows(grok.Viewlet):
+    """A base for action buttons without image
+    """
+    template = grok.PageTemplateFile('templates/widgets.pt')
+    grok.viewletmanager(WidgetsSlot)
+
+
+#
+# waeup.sirp.app.University viewlets...
+#
+class Login(grok.Viewlet):
+    """This viewlet allows to login in the sidebar.
+    """
+    grok.viewletmanager(LeftSidebar)
+    grok.context(ISIRPObject)
+    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.manageUniversity`` permission will see links defined by
+    this or derivated classes.
+    """
+    grok.viewletmanager(LeftSidebar)
+    grok.context(ISIRPObject)
+    grok.view(Interface)
+    grok.order(5)
+    # This link is only displayed, if the user is
+    # allowed to use it!
+    grok.require('waeup.managePortalConfiguration')
+
+    link = 'configuration'
+    text = _(u'Portal Configuration')
+    
+    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 ManageUsersLink(ManageLink):
+    """A link to users management, placed in upper left box.
+    """
+    grok.order(6)
+    grok.require('waeup.manageUsers')
+
+    link = u'users'
+    text = _(u'Portal Users')
+
+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 ManageDataCenter(ManageLink):
+    """A link to datacenter, placed in upper left box.
+    """
+    grok.order(6)
+    grok.require('waeup.manageDataCenter')
+
+    link = u'datacenter'
+    text = _(u'Data Center')
+
+class MyPreferences(LeftSidebarLink):
+    """A link to personal preferences, placed in upper left box.
+    """
+    grok.order(7)
+    grok.require('waeup.Public')
+    title = _(u'My Preferences')
+    icon = ''
+
+    @property
+    def url(self):
+        account_object = get_user_account(self.request)
+        if account_object:
+            return self.view.url(account_object)
+        return
+
+class MyRoles(LeftSidebarLink):
+    """A link to display site and local roles.
+    """
+    grok.order(8)
+    grok.require('waeup.Public')
+    title = _(u'My Roles')
+    icon = ''
+
+    @property
+    def url(self):
+        account_object = get_user_account(self.request)
+        if account_object:
+            return self.view.url(account_object) + '/my_roles'
+        return
+
+#
+# Manage ("Edit settings") actions...
+#
+
+class ManageDataCenterActionButton(ManageActionButton):
+    """ 'Edit settings' button for datacenter.
+    """
+    grok.context(IDataCenter)
+    grok.view(DatacenterPage)
+    text = _('Edit settings')
+
+class ManageFacultiesContainerActionButton(ManageActionButton):
+    """ 'Manage settings' button for faculties.
+    """
+    grok.context(IFacultiesContainer)
+    grok.view(FacultiesContainerPage)
+    text = _('Manage academic section')
+
+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 ManageDepartmentActionButton(ManageActionButton):
+    """ 'Manage settings' button for departments.
+    """
+    grok.context(IDepartment)
+    grok.view(DepartmentPage)
+    text = _('Manage department')
+
+class ShowDepartmentStudentsActionButton(ManageActionButton):
+    """ 'Show students' button for departments.
+    """
+    grok.context(IDepartment)
+    grok.view(DepartmentPage)
+    grok.require('waeup.showStudents')
+    icon = 'actionicon_student.png'
+    text = _('Show students')
+    target = 'showdepstudents'
+
+class ManageCourseActionButton(ManageActionButton):
+    """ 'Edit settings' button for courses.
+    """
+    grok.context(ICourse)
+    grok.view(CoursePage)
+    text = _('Edit course')
+    
+class ManageCertificateActionButton(ManageActionButton):
+    """ 'Manage settings' button for certificates.
+    """
+    grok.context(ICertificate)
+    grok.view(CertificatePage)
+    text = _('Manage certificate')
+
+class ShowCertificateStudentsActionButton(ManageActionButton):
+    """ 'Show students' button for certificates.
+    """
+    grok.context(ICertificate)
+    grok.view(CertificatePage)
+    grok.require('waeup.showStudents')
+    icon = 'actionicon_student.png'
+    text = _('Show students')
+    target = 'showcertstudents'
+
+class ManageCertificateCourseActionButton(ManageActionButton):
+    """ 'Manage settings' button for certificate courses.
+    """
+    grok.context(ICertificateCourse)
+    grok.view(CertificateCoursePage)
+    text = _('Edit course referrer')
+
+#
+# Add actions...
+#
+
+class AddUserActionButton(AddActionButton):
+    grok.require('waeup.manageUsers')
+    grok.context(IUsersContainer)
+    grok.view(UsersContainerPage)
+    text = _('Add user')
+
+#
+# Actions with a 'browse' icon...
+#
+class BrowseActionButton(ActionButton):
+    grok.baseclass()
+    grok.context(ISIRPObject)
+    grok.template('actionbutton')
+    grok.viewletmanager(ActionBar)
+    grok.require('waeup.manageUniversity')
+    icon = 'actionicon_manage.png' # File must exist in static/
+    target = '@@show' # link to this viewname.
+    text = _('Show batch logs') # Text to display on the button
+
+class BrowseDatacenterLogs(BrowseActionButton):
+    grok.context(IDataCenter)
+    grok.view(DatacenterPage)
+    grok.order(4)
+    icon = 'documentinfo_templet.png'
+    target = '@@logs'
+    text = _('Show batch logs')
+
+#
+# Misc. buttons...
+#
+class BatchOpButton(ActionButton):
+    grok.context(IDataCenter)
+    grok.view(DatacenterPage)
+    grok.require('waeup.manageUniversity')
+    grok.order(6)
+    icon = 'actionbox_templet.png'
+    target = '@@import1'
+    text = _('Batch processing')
+
+class UploadCSVButton(ActionButton):
+    grok.context(IDataCenter)
+    grok.view(DatacenterPage)
+    grok.require('waeup.manageUniversity')
+    grok.order(5)
+    icon = 'go-up-16x16.png'
+    target = '@@upload'
+    text = _('Upload CSV file')
+
+#
+# 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.manageUniversity')
+    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'
+
+# The SubobjectLister and its viewlets below are not used in SIRP.
+
+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.manageUniversity')
+
+    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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/catalog.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/catalog.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/catalog.py	(revision 7760)
@@ -0,0 +1,65 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 grok import index
+from hurry.query import Eq
+from hurry.query.interfaces import IQuery
+from hurry.query.query import Query
+from zope.catalog.catalog import ResultSet
+from zope.component import getUtility
+from zope.intid.interfaces import IIntIds
+
+from waeup.sirp.app import University
+from waeup.sirp.interfaces import IQueryResultItem
+
+# not yet used
+class SIRPQuery(Query):
+    """A hurry.query-like query that supports also ``apply``.
+    """
+    grok.implements(IQuery)
+
+    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(SIRPQuery)
+
+# not yet used
+class QueryResultItem(object):
+    grok.implements(IQueryResultItem)
+    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 = ''
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/catalog.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/catalog.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/catalog.txt	(revision 7760)
@@ -0,0 +1,235 @@
+:mod:`waeup.sirp.catalog` -- Cataloging support SIRP
+****************************************************
+
+.. module:: waeup.sirp.catalog
+
+Components that support cataloging and searching objects inside a
+SIRP site.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.SIRPUnitTestLayer
+
+.. contents::
+
+Classes
+=======
+
+:class:`SIRPQuery`
+------------------
+
+.. class:: SIRPQuery()
+
+   .. 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, `SIRPQuery` 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 SIRPQuery 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.sirp.catalog.SIRPQuery 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 SIRPQuery (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
+``SIRPQuery``:
+
+    >>> 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/configuration.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/configuration.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/configuration.py	(revision 7760)
@@ -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.sirp.interfaces import (
+    ISessionConfiguration, IConfigurationContainer, ISessionConfigurationAdd,
+    academic_sessions_vocab)
+from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/configure.zcml
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/configure.zcml	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/configure.zcml	(revision 7760)
@@ -0,0 +1,11 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:grok="http://namespaces.zope.org/grok"
+           xmlns:i18n="http://namespaces.zope.org/i18n">
+  <include package="grok" />
+  <include package="waeup.sirp" file="meta.zcml" />
+  <includeDependencies package="." />
+  <grok:grok package="." />
+  <i18n:registerTranslations directory="locales" />
+
+  <includeOverrides package="waeup.sirp.utils" file="overrides.zcml" />
+</configure>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/datacenter.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/datacenter.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/datacenter.py	(revision 7760)
@@ -0,0 +1,278 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""SIRP data center.
+
+The waeup data center cares for management of upload data and provides
+tools for importing/exporting CSV data.
+"""
+import logging
+import os
+import shutil
+import grok
+from datetime import datetime
+from zope.component import getUtility
+from zope.component.interfaces import ObjectEvent
+from waeup.sirp.interfaces import (IDataCenter, IDataCenterFile,
+                                   IDataCenterStorageMovedEvent,
+                                   IDataCenterConfig)
+from waeup.sirp.utils.helpers import copy_filesystem_tree
+from waeup.sirp.utils.logger import Logger
+
+class DataCenter(grok.Container, Logger):
+    """A data center contains CSV files.
+    """
+    grok.implements(IDataCenter)
+
+    logger_name = 'waeup.sirp.${sitename}.datacenter'
+    logger_filename = 'datacenter.log'
+
+    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']:
+            path = os.path.join(self.storage, name)
+            if os.path.exists(path):
+                continue
+            os.mkdir(path)
+        return
+
+    def getFiles(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
+            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 getLogFiles(self):
+        """Get the files from logs/ subdir. Files are sorted by name.
+        """
+        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
+            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 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 importers 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._moveFile(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
+
+
+class DataCenterFile(object):
+    """A description of a file stored in data center.
+    """
+    grok.implements(IDataCenterFile)
+
+    def __init__(self, context):
+        self.context = context
+        self.name = os.path.basename(self.context)
+        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.getctime(self.context))
+        return date.strftime("%Y-%m-%d %H:%M:%S")
+
+    def getTimeStamp(self):
+        """Get a (machine readable) timestamp.
+        """
+        return os.path.getctime(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 open(self.context, 'rb'):
+            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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/datacenter.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/datacenter.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/datacenter.txt	(revision 7760)
@@ -0,0 +1,259 @@
+SIRP Data Center
+****************
+
+The SIRP data center cares for managing CSV files and importing then.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.SIRPUnitTestLayer
+
+Creating a data center
+======================
+
+A data center can be created easily:
+
+    >>> from waeup.sirp.datacenter import DataCenter
+    >>> mydatacenter = DataCenter()
+    >>> mydatacenter
+    <waeup.sirp.datacenter.DataCenter object at 0x...>
+
+Each data center has a location in file system where files are stored:
+
+    >>> storagepath = mydatacenter.storage
+    >>> storagepath
+    '/tmp/tmp...'
+
+
+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.getFiles()
+    []
+
+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.getFiles()
+    [<waeup.sirp.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.getFiles()[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))
+    ['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))
+    ['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))
+    ['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))
+    ['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))
+    ['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.
+
+Clean up:
+
+    >>> shutil.rmtree(verynewpath)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/frontpage.rst
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/frontpage.rst	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/frontpage.rst	(revision 7760)
@@ -0,0 +1,69 @@
+Welcome to the Student Information and Registration Portal
+==========================================================
+
+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 im Studenteninformations- und Registrierungsportal
+=============================================================
+
+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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/ftesting.zcml
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/ftesting.zcml	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/ftesting.zcml	(revision 7760)
@@ -0,0 +1,68 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   xmlns:sirp="http://namespaces.waeup.org/sirp"
+   i18n_domain="waeup.sirp"
+   package="waeup.sirp"
+   >
+
+  <include package="grok" />
+  <includeOverrides package="waeup.sirp" />
+  <include package="waeup.sirp" file="mail.zcml" />
+
+  <!-- Where should the datacenter reside by default? -->
+  <sirp: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"
+      />
+
+
+  <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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/__init__.py	(revision 7760)
@@ -0,0 +1,11 @@
+"""This package contains everything regarding hostels.
+"""
+# Make this a package.
+from waeup.sirp.hostels.container import HostelsContainer
+from waeup.sirp.hostels.hostel import Hostel, Bed
+
+__all__ = [
+    'HostelsContainer',
+    'Hostel',
+    'Bed',
+    ]
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/browser.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/browser.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/browser.py	(revision 7760)
@@ -0,0 +1,331 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.browser import (
+    SIRPEditFormPage, SIRPAddFormPage, SIRPDisplayFormPage,
+    NullValidator)
+from waeup.sirp.browser.breadcrumbs import Breadcrumb
+from waeup.sirp.browser.resources import datepicker, datatable, tabs, warning
+from waeup.sirp.browser.layout import default_primary_nav_template
+from waeup.sirp.browser.pages import delSubobjects
+from waeup.sirp.browser.viewlets import (
+    ManageActionButton, PrimaryNavTab)
+from waeup.sirp.browser.layout import jsaction, action
+from waeup.sirp.interfaces import ISIRPObject, ISIRPUtils
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.hostels.vocabularies import NOT_OCCUPIED
+from waeup.sirp.hostels.hostel import Hostel
+from waeup.sirp.hostels.interfaces import (
+    IHostelsContainer, IHostel, IBed, IBedAllocateStudent)
+
+def write_log_message(view, message):
+    ob_class = view.__implemented__.__name__.replace('waeup.sirp.','')
+    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(ISIRPObject)
+    grok.order(5)
+    grok.require('waeup.viewHostels')
+    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.getBedCoordinates()
+        return _('Block ${a}, Room ${b}, Bed ${c}',
+            mapping = {'a':co[1], 'b':co[2], 'c':co[3]})
+
+class HostelsContainerPage(SIRPDisplayFormPage):
+    """The standard view for hostels containers.
+    """
+    grok.context(IHostelsContainer)
+    grok.name('index')
+    grok.require('waeup.viewHostels')
+    grok.template('containerpage')
+    label = _('Accommodation Section')
+    pnav = 5
+
+class HostelsContainerManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IHostelsContainer)
+    grok.view(HostelsContainerPage)
+    grok.require('waeup.manageHostels')
+    text = _('Manage accommodation section')
+
+class HostelsContainerManagePage(SIRPDisplayFormPage):
+    """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')
+
+    def update(self):
+        warning.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 form.has_key('val_id'):
+            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
+
+class HostelAddFormPage(SIRPAddFormPage):
+    """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')
+    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(SIRPDisplayFormPage):
+    """ Page to display hostel data
+    """
+    grok.context(IHostel)
+    grok.name('index')
+    grok.require('waeup.viewHostels')
+    #grok.template('hostelpage')
+    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(SIRPEditFormPage):
+    """ View to edit hostel data
+    """
+    grok.context(IHostel)
+    grok.name('manage')
+    grok.require('waeup.manageHostels')
+    form_fields = grok.AutoFields(IHostel).omit('hostel_id')
+    form_fields['beds_reserved'].for_display = True
+    grok.template('hostelmanagepage')
+    label = _('Manage hostel')
+    pnav = 5
+    taboneactions = [_('Save')]
+    tabtwoactions = [_('Update all beds'),
+        _('Switch reservation of selected beds'),
+        _('Release selected beds')]
+    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()
+        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 form.has_key('val_id'):
+            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
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        preferred_language = self.request.cookies.get(
+            'sirp.language', portal_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.sirp',
+                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 form.has_key('val_id'):
+            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
+
+class BedManageFormPage(SIRPEditFormPage):
+    """ View to edit bed data
+    """
+    grok.context(IBedAllocateStudent)
+    grok.name('index')
+    grok.require('waeup.manageHostels')
+    form_fields = grok.AutoFields(IBedAllocateStudent).omit(
+        'bed_id').omit('bed_number').omit('bed_type')
+    label = _('Allocate student')
+    pnav = 5
+
+    @action(_('Save'))
+    def save(self, **data):
+        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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/browser_templates/containermanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/browser_templates/containermanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/browser_templates/containermanagepage.pt	(revision 7760)
@@ -0,0 +1,29 @@
+<form action="." tal:attributes="action request/URL"
+    i18n:domain="waeup.sirp"
+    method="POST" enctype="multipart/form-data">
+  <table>
+    <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:replace="structure action/render"/>
+    </span>
+  </div>
+</form>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/browser_templates/containerpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/browser_templates/containerpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/browser_templates/containerpage.pt	(revision 7760)
@@ -0,0 +1,20 @@
+<div i18n:domain="waeup.sirp"
+   i18n:translate="" tal:condition="python: not len(context.keys())">
+There no subobjects registered yet.
+</div>
+
+<table i18n:domain="waeup.sirp">
+  <thead>
+    <tr>
+      <th i18n:translate="">Id</th>
+      <th i18n:translate="">Name</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="value context/values">
+      <td> <a tal:attributes="href value/__name__">
+          <span tal:content="value/hostel_id">ID</span></a></td>
+      <td tal:content="value/hostel_name">NAME</td>
+    </tr>
+  </tbody>
+</table>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/browser_templates/hostelmanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/browser_templates/hostelmanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/browser_templates/hostelmanagepage.pt	(revision 7760)
@@ -0,0 +1,79 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.sirp" 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" />
+      </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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/catalog.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/catalog.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/catalog.py	(revision 7760)
@@ -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.sirp.interfaces import IUniversity
+from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/container.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/container.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/container.py	(revision 7760)
@@ -0,0 +1,62 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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
+from waeup.sirp.hostels.interfaces import IHostelsContainer, IHostel
+from waeup.sirp.utils.logger import Logger
+
+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
+
+    logger_name = 'waeup.sirp.${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)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/hostel.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/hostel.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/hostel.py	(revision 7760)
@@ -0,0 +1,223 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+These are the hostels.
+"""
+import grok
+from zope.event import notify
+from datetime import datetime
+from waeup.sirp.utils.helpers import attrs_to_fields
+from waeup.sirp.hostels.vocabularies import NOT_OCCUPIED
+from waeup.sirp.hostels.interfaces import IHostel, IBed, IBedAllocateStudent
+from waeup.sirp.students.interfaces import IBedTicket
+from waeup.sirp.interfaces import MessageFactory as _
+
+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)
+
+    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 self.has_key(uid):
+                            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
+                        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, IBedAllocateStudent)
+    grok.provides(IBed)
+
+    def getBedCoordinates(self):
+        """Determine the coordinates from the bed_id.
+        """
+        return self.bed_id.split('_')
+
+    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.getBedCoordinates()
+        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
+        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()[
+                'configuration'].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
+            timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+            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)
+
+@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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/interfaces.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/interfaces.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/interfaces.py	(revision 7760)
@@ -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
+##
+from zope.interface import invariant, Invalid
+from zope import schema
+from waeup.sirp.interfaces import ISIRPObject
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.hostels.vocabularies import (
+    bed_letters, blocks, special_handling, StudentSource)
+
+class IHostelsContainer(ISIRPObject):
+    """A container for all kind of hostel objects.
+
+    """
+
+class IHostel(ISIRPObject):
+    """A base representation of hostels.
+
+    """
+
+    def loggerInfo(ob_class, comment):
+        """Adds an INFO message to the log file
+        """
+
+    def updateBeds():
+        """Fill hostel with beds or update beds.
+        """
+
+    hostel_id = schema.TextLine(
+        title = _(u'Hostel Id'),
+        readonly = True,
+        )
+
+    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_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'),
+        vocabulary = special_handling,
+        required = True,
+        default = u'regular',
+        )
+
+    @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_all)
+        if len(beds) != len(set(beds)):
+            raise Invalid(_('Bed categories overlap.'))
+
+class IBed(ISIRPObject):
+    """A base representation of beds.
+
+    """
+
+    def loggerInfo(ob_class, comment):
+        """Adds an INFO message to the log file
+        """
+
+    def getBedCoordinates():
+        """Determine the coordinates from bed_id.
+        """
+    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)'),
+        required = True,
+        default = u'',
+        )
+
+
+
+class IBedAllocateStudent(IBed):
+    """A representation of beds for allocation form only.
+
+    """
+
+    owner = schema.Choice(
+        title = _(u'Owner (Student)'),
+        source = StudentSource(),
+        default = None,
+        required = True,
+        )
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/permissions.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/permissions.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/permissions.py	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/tests.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/tests.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/tests.py	(revision 7760)
@@ -0,0 +1,337 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 shutil
+import tempfile
+import grok
+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.sirp.app import University
+from waeup.sirp.hostels.interfaces import (
+    IHostelsContainer, IHostel, IBed)
+from waeup.sirp.hostels.container import HostelsContainer
+from waeup.sirp.hostels.hostel import Hostel, Bed
+from waeup.sirp.testing import (FunctionalLayer, FunctionalTestCase)
+from waeup.sirp.students.student import Student
+from waeup.sirp.students.accommodation import BedTicket
+from waeup.sirp.university.department import Department
+
+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)
+
+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['configuration'].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'xyz'
+        bed.bed_number = 1
+        bed.bed_type = u'abc'
+        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
+
+    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'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']['xyz']
+
+    def test_search_by_owner(self):
+        # We can find a certain bed
+        myobj = self.app['hostels']['hall-x']['xyz']
+        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']['xyz']
+
+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 for x in results]
+        assert len(results) == 4
+        # 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)
+        assert self.app['hostels']['hall-1'][
+            'hall-1_A_101_D'].bed_type == 'regular_female_reserved'
+        self.assertTrue('<li>A_101_A</li>' in self.browser.contents)
+        # Change hostel configuration
+        hall.beds_for_all = ['D']
+        self.browser.getControl("Update all beds").click()
+        expected = '...10 empty beds removed, 8 beds added, 0 occupied beds modified...'
+        self.assertMatches(expected,self.browser.contents)
+        results = cat.searchResults(
+            bed_type=('regular_female_all', 'regular_female_all'))
+        results = [x for x in results]
+        assert len(results) == 1
+        # 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.ticket_id = u'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> --')
+        # 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 studenst after cancellation
+        self.browser.open(self.container_path + '/hall-1/hall-1_A_101_A')
+        self.browser.getControl(name="form.owner").value = [self.student_id]
+        self.browser.getControl("Save").click()
+        self.assertMatches("...Form has been saved...", 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()
+        # 7 beds have been removed and re-added, 1 bed remains untouched
+        expected = '...7 empty beds removed, 7 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 = '...7 empty beds removed, 7 beds added, ' + \
+            '1 occupied beds modified (hall-1_A_101_A )...'
+        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)
+        # 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
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/vocabularies.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/vocabularies.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/hostels/vocabularies.py	(revision 7760)
@@ -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
+##
+"""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.sirp.interfaces import SimpleSIRPVocabulary
+from waeup.sirp.interfaces import MessageFactory as _
+
+NOT_OCCUPIED = u'not occupied'
+
+class StudentSource(BasicContextualSourceFactory):
+    """A students source delivers all students in accommodation session.
+    """
+
+    def acco_students(self, context):
+        catalog = getUtility(ICatalog, name='students_catalog')
+        accommodation_session = getSite()['configuration'].accommodation_session
+        students = catalog.searchResults(current_session=(
+            accommodation_session,accommodation_session))
+        existing_students = [
+            context.__parent__[key].owner
+            for key in context.__parent__.keys()]
+        students = [student for student in students
+                      if not student.student_id in existing_students]
+        students = sorted(list(students),
+                          key=lambda value: value.student_id)
+        return dict([(student.student_id,student.fullname) for student in students])
+
+    def getValues(self, context):
+        return self.acco_students(context).keys()
+
+    def getToken(self, context, value):
+        return value
+
+    def getTitle(self, context, value):
+        return "%s - %s" % (value, self.acco_students(context)[value])
+
+bed_letters = SimpleSIRPVocabulary(
+    (_('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'),
+    )
+
+blocks = SimpleSIRPVocabulary(
+    (_('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 K'),'K'),
+    (_('Block L'),'L'),
+    (_('Block M'),'M'),
+    (_('Block N'),'N'),
+    (_('Block O'),'O'),
+    (_('Block P'),'P'),
+    )
+
+special_handling = SimpleSIRPVocabulary(
+    (_('Regular Hostel'),'regular'),
+    (_('Blocked Hostel'),'blocked'),
+    (_('Postgraduate Hostel'),'pg'),
+    )
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/README.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/README.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/README.txt	(revision 7760)
@@ -0,0 +1,216 @@
+waeup.sirp.image -- handling image files
+========================================
+
+The image file widget is built on top of the :class:`SIRPImageFile` object::
+
+  >>> from waeup.sirp.image import SIRPImageFile
+  >>> file = SIRPImageFile('foo.jpg', 'mydata')
+  >>> file.filename
+  'foo.jpg'
+  >>> file.data
+  'mydata'
+  >>> file.size
+  6
+  >>> f = file.file
+  >>> f.read()
+  'mydata'
+
+We can also create SIRPImageFile 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 SIRPImageFile 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 = SIRPImageFile('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 = SIRPImageFile('foo.jpg', 'data')
+  >>> f = file.file
+  >>> f.read()
+  'data'
+  >>> file.size
+  4
+
+Clean up:
+
+  >>> import shutil
+  >>> shutil.rmtree(dirpath)
+
+Support for :mod:`waeup.sirp.imagestorage`
+------------------------------------------
+
+The behaviour shown above can be used for any Zope3 application. With
+:mod:`waeup.sirp` we use a special file retrieval utility defined in
+:mod:`waeup.sirp.imagestorage`. As this utility is based on
+:mod:`waeup.sirp` 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.sirp.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.sirp.image.image.SIRPImageFile'>, 'foo')
+
+which means: `ImageFile` fields should better contain
+:class:`SIRPImageFile` instances.
+
+We can store normal :class:`SIRPImageFile` instances:
+
+  >>> field.validate(SIRPImageFile('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(
+  ...     SIRPImageFile('bar.jpg', '123456789012')) is None
+  True
+
+  >>> field.validate(
+  ...     SIRPImageFile('bar.jpg', '12345')) is None
+  True
+
+the following are not:
+
+  >>> field.validate(
+  ...     SIRPImageFile('bar.jpg', '1234567890123'))
+  Traceback (most recent call last):
+  ...
+  TooBig: ('bar.jpg', '13 bytes (max: 12 bytes)')
+
+  >>> field.validate(
+  ...     SIRPImageFile('bar.jpg', '1234'))
+  Traceback (most recent call last):
+  ...
+  TooSmall: ('bar.jpg', '4 bytes (min: 5 bytes)')
+
+
+Broken Unequality Comparison
+----------------------------
+
+SIRPImageFile does not reproduce the broken unequal comparison from
+its base:
+
+  >>> f1 = SIRPImageFile('bar.jpg', '123456789')
+  >>> f2 = SIRPImageFile('bar.jpg', '123456789')
+  >>> f3 = SIRPImageFile('baz.jpg', '1234')
+  >>> f1 == f2
+  True
+
+  >>> f1 != f2
+  False
+
+  >>> f1 == f3
+  False
+
+  >>> f1 != f3
+  True
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/__init__.py	(revision 7760)
@@ -0,0 +1,10 @@
+"""Image file components.
+
+Includings schemas, widgets and the content components.
+"""
+from waeup.sirp.image.image import SIRPImageFile, createSIRPImageFile
+
+__all__ = [
+    "SIRPImageFile",
+    "createSIRPImageFile",
+    ]
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/__init__.py	(revision 7760)
@@ -0,0 +1,10 @@
+# The public API part
+# Contains the widgets defined for image files.
+from waeup.sirp.image.browser.widget import (
+    EncodingImageFileWidget, ThumbnailWidget,
+    )
+
+__all__ = (
+    EncodingImageFileWidget,
+    ThumbnailWidget,
+    )
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/tests/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/tests/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/tests/__init__.py	(revision 7760)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/tests/image.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/tests/image.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/tests/image.txt	(revision 7760)
@@ -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.sirp.image import SIRPImageFile
+  >>> testimage = os.path.join(os.path.dirname(__file__), 'sample.jpg')
+  >>> testimage2 = os.path.join(os.path.dirname(__file__), 'sample2.jpg')
+  >>> some_file = SIRPImageFile('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.sirp.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.sirp.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.sirp.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 = SIRPImageFile('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.sirp.image.SIRPImageFile` 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 = SIRPImageFile(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 SIRPImageFile object with that
+file_id stored:
+
+  >>> from waeup.sirp.image import createSIRPImageFile
+  >>> image = createSIRPImageFile(
+  ...     '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 createSIRPImageFile 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/tests/test_image.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/tests/test_image.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/tests/test_image.py	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/tests/test_views.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/tests/test_views.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/tests/test_views.py	(revision 7760)
@@ -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.sirp.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.sirp.image.browser.views` module.
+    """
+    @classmethod
+    def setUp(cls):
+        grok.testing.grok('waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/views.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/views.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/views.py	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/widget.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/widget.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/browser/widget.py	(revision 7760)
@@ -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.sirp.image import SIRPImageFile
+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 SIRPImageFile(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 SIRPImageFile(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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/image.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/image.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/image.py	(revision 7760)
@@ -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.sirp.image.interfaces import ISIRPImageFile
+
+class SIRPImageFile(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:`SIRPImageFile` you should use
+    :func:`createSIRPImageFile`.
+    """
+    implements(ISIRPImageFile)
+
+    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 createSIRPImageFile(filename, f):
+    retrieval = getUtility(IFileRetrieval)
+    return retrieval.createFile(filename, f)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/interfaces.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/interfaces.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/interfaces.py	(revision 7760)
@@ -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.sirp.image
+"""
+from hurry.file.interfaces import IFile, IFileRetrieval, IHurryFile
+
+class IImageFile(IFile):
+    """Image file field.
+    """
+    
+class ISIRPImageFile(IHurryFile):
+    """Image file.
+    """
+    
+class IImageFileRetrieval(IFileRetrieval):
+    """A file retrieval for images.
+    """
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/schema.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/schema.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/schema.py	(revision 7760)
@@ -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.sirp.image.interfaces import IImageFile
+from waeup.sirp.image.image import SIRPImageFile
+
+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 = SIRPImageFile
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/tests.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/tests.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/image/tests.py	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/imagestorage.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/imagestorage.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/imagestorage.py	(revision 7760)
@@ -0,0 +1,578 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp``. 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.sirp.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.sirp.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.sirp.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.sirp.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.sirp.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.sirp 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.sirp.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.sirp.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.sirp.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.sirp.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.sirp.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.sirp.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'`` and so on, if it exists.
+        """
+        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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/index.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/index.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/index.txt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/interfaces.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/interfaces.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/interfaces.py	(revision 7760)
@@ -0,0 +1,972 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 zope.i18nmessageid
+from datetime import datetime
+from hurry.file.interfaces import IFileRetrieval
+from hurry.workflow.interfaces import IWorkflow, 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.container.interfaces import INameChooser
+from zope.interface import Interface, Attribute, implements
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+_ = MessageFactory = zope.i18nmessageid.MessageFactory('waeup.sirp')
+
+CREATED = 'created'
+ADMITTED = 'admitted'
+CLEARANCE = 'clearance started'
+REQUESTED = 'clearance requested'
+CLEARED = 'cleared'
+PAID = 'school fee paid'
+RETURNING = 'returning'
+REGISTERED = 'courses registered'
+VALIDATED = 'courses validated'
+
+default_frontpage = u'' + codecs.open(os.path.join(
+        os.path.dirname(__file__), 'frontpage.rst'),
+        encoding='utf-8', mode='rb').read()
+
+def SimpleSIRPVocabulary(*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 year_range():
+    curr_year = datetime.now().year
+    return range(curr_year - 4, curr_year + 5)
+
+def academic_sessions():
+    curr_year = datetime.now().year
+    year_range = range(curr_year - 10, curr_year + 2)
+    return [('%s/%s' % (year,year+1), year) for year in year_range]
+
+academic_sessions_vocab = SimpleSIRPVocabulary(*academic_sessions())
+
+registration_states_vocab = SimpleSIRPVocabulary(
+    (_('created'), CREATED),
+    (_('admitted'), ADMITTED),
+    (_('clearance started'), CLEARANCE),
+    (_('clearance requested'), REQUESTED),
+    (_('cleared'), CLEARED),
+    (_('school fee paid'), PAID),
+    (_('returning'), RETURNING),
+    (_('courses registered'), REGISTERED),
+    (_('courses validated'), VALIDATED),
+    )
+
+# Define a valiation method for email addresses
+class NotAnEmailAddress(schema.ValidationError):
+    __doc__ = u"Invalid email address"
+
+check_email = re.compile(
+    r"[a-zA-Z0-9._%-']+@([a-zA-Z0-9-]+.)*[a-zA-Z]{2,4}").match
+
+def validate_email(value):
+    if not check_email(value):
+        raise NotAnEmailAddress(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.sirp.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.sirp.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.s.interface.
+            from waeup.sirp.browser.interfaces import ICaptchaManager
+        except:
+            return captchas
+        return sorted(getUtility(ICaptchaManager).getAvailCaptchas().keys())
+
+    def getTitle(self, value):
+        return value
+
+class ISIRPUtils(Interface):
+    """A collection of methods which are subject to customization.
+    """
+
+    def storage():
+        """Return the initial storage path of the data center.
+        """
+
+    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, login_url, 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.
+        """
+
+    def getVerdictsDict():
+        """Provide a dict of verdicts.
+        """
+
+class ISIRPObject(Interface):
+    """A SIRP object.
+
+    This is merely a marker interface.
+    """
+
+class IUniversity(ISIRPObject):
+    """Representation of a university.
+    """
+
+
+class ISIRPContainer(ISIRPObject):
+    """A container for SIRP objects.
+    """
+
+class ISIRPContained(ISIRPObject):
+    """An item contained in an ISIRPContainer.
+    """
+
+class ICSVExporter(Interface):
+    """A CSV file exporter for objects.
+    """
+    fields = Attribute("""List of fieldnames in resulting CSV""")
+    
+    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 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, filapath=None):
+        """Export all items in `site` as CSV file.
+
+        if `filepath` is not given, a string with the data should be returned.
+        """
+
+class ISIRPExporter(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 ISIRPXMLExporter(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 ISIRPXMLImporter(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'Importer name'
+        )
+
+    mode = schema.Choice(
+        title = u'Import mode',
+        values = ['create', 'update', 'remove']
+        )
+
+    def doImport(path, headerfields, mode='create', user='Unknown',
+                 logger=None):
+        """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.
+        """
+
+class IContactForm(ISIRPObject):
+    """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 ISIRPPrincipalInfo(IPrincipalInfo):
+    """Infos about principals that are users of SIRP SIRP.
+    """
+    email = Attribute("The email address of a user")
+    phone = Attribute("The phone number of a user")
+
+
+class ISIRPPrincipal(IPrincipal):
+    """A principle for SIRP SIRP.
+
+    This interface extends zope.security.interfaces.IPrincipal and
+    requires also an `id` and other attributes defined there.
+    """
+
+    email = schema.TextLine(
+        title = u'Email',
+        description = u'',
+        required=False,)
+
+    phone = schema.TextLine(
+        title = u'Phone',
+        description = u'',
+        required=False,)
+
+class IUserAccount(ISIRPObject):
+    """A user account.
+    """
+    name = schema.TextLine(
+        title = u'User ID',
+        description = u'Login name of user',
+        required = True,)
+
+    title = schema.TextLine(
+        title = u'Full Name',
+        required = False,)
+
+    description = schema.Text(
+        title = u'Description/Notice',
+        required = False,)
+
+    email = schema.ASCIILine(
+        title = u'Email',
+        default = None,
+        required = True,
+        constraint=validate_email,
+        )
+
+    phone = schema.TextLine(
+        title = u'Phone',
+        default = None,
+        required = True,
+        )
+
+    roles = schema.List(
+        title = u'Portal roles',
+        value_type = schema.Choice(source=RoleSource()))
+
+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(ISIRPObject):
+    """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(ISIRPObject):
+    """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.SIRP',
+        required = True,
+        )
+
+    skin = schema.Choice(
+        title = u'Skin',
+        default = u'gray waeup theme',
+        vocabulary = 'waeup.sirp.browser.theming.ThemesVocabulary',
+        required = True,
+        )
+
+    frontpage = schema.Text(
+        title = u'Content in reST format',
+        required = False,
+        default = default_frontpage,
+        )
+
+    frontpage_dict = schema.Dict(
+        title = u'Content as language dictionary with values in html format',
+        required = False,
+        default = {},
+        )
+
+    accommodation_session = schema.Choice(
+        title = u'Accommodation Booking Session',
+        source = academic_sessions_vocab,
+        default = datetime.now().year,
+        required = False,
+        readonly = False,
+        )
+
+    accommodation_states = schema.List(
+        title = u'Allowed States for Accommodation Booking',
+        value_type = schema.Choice(
+            vocabulary = registration_states_vocab,
+            ),
+        default = [],
+        )
+
+    name_admin = schema.TextLine(
+        title = u'Name of Administrator',
+        default = u'Administrator',
+        required = False,
+        )
+
+    email_admin = schema.ASCIILine(
+        title = u'Email Address of Administrator',
+        default = 'contact@waeup.org',
+        required = False,
+        constraint=validate_email,
+        )
+
+    email_subject = schema.TextLine(
+        title = u'Subject of Email to Administrator',
+        default = u'SIRP Contact',
+        required = False,
+        )
+
+    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,
+        )
+
+class ISessionConfiguration(ISIRPObject):
+    """A session configuration object.
+    """
+
+    academic_session = schema.Choice(
+        title = u'Academic Session',
+        source = academic_sessions_vocab,
+        default = None,
+        required = True,
+        readonly = True,
+        )
+
+    school_fee_base = schema.Int(
+        title = u'School Fee',
+        default = 0,
+        )
+
+    surcharge_1 = schema.Int(
+        title = u'Surcharge 1',
+        default = 0,
+        )
+
+    surcharge_2 = schema.Int(
+        title = u'Surcharge 2',
+        default = 0,
+        )
+
+    surcharge_3 = schema.Int(
+        title = u'Surcharge 3',
+        default = 0,
+        )
+
+    clearance_fee = schema.Int(
+        title = u'Clearance Fee',
+        default = 0,
+        )
+
+    booking_fee = schema.Int(
+        title = u'Booking Fee',
+        default = 0,
+        )
+
+    acceptance_fee = schema.Int(
+        title = u'Acceptance Fee',
+        default = 0,
+        )
+
+    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(ISIRPObject):
+    """A data center.
+
+    TODO : declare methods, at least those needed by pages.
+    """
+    pass
+
+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 ISIRPPluggable(Interface):
+    """A component that might be plugged into a SIRP SIRP app.
+
+    Components implementing this interface are referred to as
+    'plugins'. They are normally called when a new
+    :class:`waeup.sirp.app.University` instance is created.
+
+    Plugins can setup and update parts of the central site without the
+    site object (normally a :class:`waeup.sirp.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.sirp.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 IObjectHistory(Interface):
+
+    messages = schema.List(
+        title = u'List of messages stored',
+        required = True,
+        )
+
+    def addMessage(message):
+        """Add a message.
+        """
+
+class ISIRPWorkflowInfo(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 SIRPLoggers")
+
+    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 SIRP 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 SIRPImageFile 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):
+        """Create a bytestream representing a PDF from context.
+
+        If `view` is passed in additional infos might be rendered into
+        the document.
+        """
+
+class IMailService(Interface):
+    """A mail service.
+    """
+
+    def __call__():
+        """Get the default mail delivery.
+        """
+
+from zope.configuration.fields import Path
+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,
+        )
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/language.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/language.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/language.py	(revision 7760)
@@ -0,0 +1,41 @@
+## $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 grokcore.component
+from zope.component import getUtility
+from zope.publisher.interfaces.browser import IBrowserRequest
+from zope.i18n.interfaces import IUserPreferredLanguages
+from waeup.sirp.configuration import ConfigurationContainer
+from waeup.sirp.interfaces import ISIRPUtils
+
+class SIRPLanguage(grokcore.component.Adapter):
+    """Set preferred languages"""
+    grokcore.component.context(IBrowserRequest)
+    grokcore.component.implements(IUserPreferredLanguages)
+
+    def getPreferredLanguages(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+
+        # 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('sirp.language', portal_language)
+
+        # According to IUserPreferredLanguages, we must return a list.
+        return [lang]
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/de/LC_MESSAGES/waeup.sirp.po
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/de/LC_MESSAGES/waeup.sirp.po	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/de/LC_MESSAGES/waeup.sirp.po	(revision 7760)
@@ -0,0 +1,3799 @@
+# #############################################################################
+#
+# 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.SIRP\n"
+"POT-Creation-Date: Fri Mar  2 12:58:47 2012\n"
+"PO-Revision-Date: 2012-03-02 22:10+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\\sirp\\trunk\\src\n"
+
+#: waeup/sirp/accesscodes/browser.py:100
+#: waeup/sirp/accesscodes/browser.py:113
+#: waeup/sirp/applicants/browser.py:80
+#: waeup/sirp/applicants/browser.py:130
+#: waeup/sirp/applicants/browser.py:182
+#: waeup/sirp/applicants/browser.py:254
+#: waeup/sirp/applicants/browser.py:255
+#: waeup/sirp/applicants/browser.py:338
+#: waeup/sirp/browser/pages.py:503
+#: waeup/sirp/browser/pages.py:708
+#: waeup/sirp/browser/pages.py:735
+#: waeup/sirp/browser/pages.py:758
+#: waeup/sirp/browser/pages.py:805
+#: waeup/sirp/browser/pages.py:835
+#: waeup/sirp/browser/pages.py:955
+#: waeup/sirp/browser/pages.py:1183
+#: waeup/sirp/browser/pages.py:1277
+#: waeup/sirp/browser/pages.py:1298
+#: waeup/sirp/browser/pages.py:1325
+#: waeup/sirp/browser/pages.py:1353
+#: waeup/sirp/browser/pages.py:1354
+#: waeup/sirp/browser/pages.py:1400
+#: waeup/sirp/browser/pages.py:1441
+#: waeup/sirp/browser/pages.py:1520
+#: waeup/sirp/browser/pages.py:1521
+#: waeup/sirp/browser/pages.py:1523
+#: waeup/sirp/browser/pages.py:1601
+#: waeup/sirp/browser/pages.py:1646
+#: waeup/sirp/browser/pages.py:1684
+#: waeup/sirp/browser/pages.py:1728
+#: waeup/sirp/browser/pages.py:1767
+#: waeup/sirp/browser/pages.py:1769
+#: waeup/sirp/browser/pages.py:1804
+#: waeup/sirp/browser/pages.py:1855
+#: waeup/sirp/browser/pages.py:1907
+#: waeup/sirp/students/browser.py:584
+#: waeup/sirp/students/browser.py:585
+#: waeup/sirp/students/browser.py:746
+#: waeup/sirp/students/browser.py:748
+#: waeup/sirp/students/browser.py:888
+msgid "Cancel"
+msgstr "Abbruch"
+
+#: waeup/sirp/accesscodes/browser.py:102
+msgid "Batch creation cancelled."
+msgstr "Zugangscodeerzeugung abgebrochen."
+
+#: waeup/sirp/accesscodes/browser.py:112
+msgid "Reimport"
+msgstr "Reimportieren"
+
+#: waeup/sirp/accesscodes/browser.py:115
+msgid "Reimport Access Code Batches"
+msgstr "Zugangscodeserie reimportieren"
+
+#: waeup/sirp/accesscodes/browser.py:120
+msgid "Reimport cancelled."
+msgstr "Reimport abgebrochen."
+
+#: waeup/sirp/accesscodes/browser.py:126
+msgid "No file chosen. Action cancelled."
+msgstr "Keine Datei ausgewählt, Aktion abgebrochen."
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:136
+msgid "This batch already exists: ${a}"
+msgstr "Diese Serie existiert bereits: ${a}"
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:139
+msgid "Successfully reimported: ${a}"
+msgstr "Erfolgreich reimportiert: ${a}"
+
+#: waeup/sirp/accesscodes/browser.py:152
+msgid "Search and Manage Access Codes"
+msgstr "Suche und verwalte Zugangscodes"
+
+#: waeup/sirp/accesscodes/browser.py:153
+#: waeup/sirp/browser/pages.py:592
+#: waeup/sirp/browser/pages.py:1142
+#: waeup/sirp/browser/viewlets.py:190
+#: waeup/sirp/students/browser.py:171
+#: waeup/sirp/students/browser.py:205
+msgid "Search"
+msgstr "Suchen"
+
+#: waeup/sirp/accesscodes/browser.py:154
+msgid "Disable ACs"
+msgstr "Zugangscode deaktivieren"
+
+#: waeup/sirp/accesscodes/browser.py:155
+msgid "Enable ACs"
+msgstr "Zugangscode aktivieren"
+
+#: waeup/sirp/accesscodes/browser.py:180
+#: waeup/sirp/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr "deaktiviert"
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:182
+msgid "${a} disabled."
+msgstr "${a} deaktiviert"
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:186
+msgid "${a}: Disable transition not allowed."
+msgstr "${a}: Deaktivierung nicht möglich."
+
+#: waeup/sirp/accesscodes/browser.py:190
+msgid "re-enabled"
+msgstr "reaktiviert"
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:192
+msgid "${a} (re-)enabled."
+msgstr "${a} (re-)aktiviert."
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:196
+msgid "${a}: Re-enable transition not allowed."
+msgstr "${a}: Reaktivieren nich möglich."
+
+#: waeup/sirp/accesscodes/browser.py:235
+msgid "Add Access Code Batch"
+msgstr "Zugangscodeserie hinzufügen"
+
+#: waeup/sirp/accesscodes/browser.py:244
+msgid "Reimport Access Code Batch"
+msgstr "Zugangscodeserie reimportieren"
+
+#: waeup/sirp/accesscodes/browser.py:252
+msgid "Search Access Codes"
+msgstr "Zugangscode suchen"
+
+#: waeup/sirp/accesscodes/browser.py:261
+msgid "Access Codes"
+msgstr "Zugangscodes"
+
+#: waeup/sirp/accesscodes/browser.py:43
+msgid "Archive"
+msgstr "Archivieren"
+
+#: waeup/sirp/accesscodes/browser.py:44
+msgid "Archive and delete"
+msgstr "Archivieren und löschen"
+
+#: waeup/sirp/accesscodes/browser.py:46
+#: waeup/sirp/accesscodes/browser.py:206
+#: waeup/sirp/accesscodes/browser.py:226
+msgid "Access Code Batches"
+msgstr "Zugangscodeserien"
+
+#: waeup/sirp/accesscodes/browser.py:53
+msgid "No batch selected."
+msgstr "Keine Serie ausgewählt."
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:61
+msgid "Archived ${a} (${b})"
+msgstr "${a} (${b}) archiviert"
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:68
+msgid "Deleted batch ${a}"
+msgstr "Serie ${a} gelöscht"
+
+#: waeup/sirp/accesscodes/browser.py:77
+msgid "Create Access Code Batch"
+msgstr "Zugangscodeserie erzeugen"
+
+#: waeup/sirp/accesscodes/browser.py:83
+msgid "Create batch"
+msgstr "Serie erzeugen"
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:91
+msgid "Batch created (${a} entries)"
+msgstr "Serie ${a} mit Einträgen erzeugt."
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:93
+msgid "Data written to ${a}"
+msgstr "Daten in ${a} gespeichert."
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:10
+msgid "Entries/(invalidated)"
+msgstr "Einträge / (verbraucht)"
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:11
+msgid "Cost"
+msgstr "Preis"
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:12
+msgid "Date of Creation"
+msgstr "Erzeugungsdatum"
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:13
+msgid "Creator"
+msgstr "Erzeuger"
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:2
+msgid "The following batches are available:"
+msgstr "Die folgenden Serien sind vorhanden:"
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:39
+msgid "No batches yet"
+msgstr "Keine Serie vorhanden"
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:9
+msgid "Prefix"
+msgstr "Prefix"
+
+#: waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt:23
+msgid "No import batches available"
+msgstr "Keine Importserien verfügbar"
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:23
+msgid "Serial"
+msgstr "Seriennummer"
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:24
+msgid "AC"
+msgstr "Zugangscode"
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:26
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:38
+msgid "Owner"
+msgstr "Eigentümer"
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:5
+msgid "for access codes"
+msgstr "nach Zugangscodes"
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:7
+msgid "with PIN"
+msgstr "mit PIN"
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:8
+msgid "with serial"
+msgstr "mit Seriennummer"
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:9
+msgid "with message term"
+msgstr "mit Mitteilung"
+
+#: waeup/sirp/accesscodes/interfaces.py:29
+msgid "Serial number inside batch"
+msgstr "Seriennummer innerhalb einer Serie"
+
+#: waeup/sirp/accesscodes/interfaces.py:32
+msgid "Prefix inside batch"
+msgstr "Prefix innerhalb einer Serie"
+
+#: waeup/sirp/accesscodes/interfaces.py:35
+msgid "Batch number"
+msgstr "Batch-Nummer"
+
+#: waeup/sirp/accesscodes/interfaces.py:38
+msgid "Random part of access code"
+msgstr "Zufallsteil des Zugangscodes"
+
+#: waeup/sirp/accesscodes/interfaces.py:41
+#: waeup/sirp/accesscodes/interfaces.py:80
+msgid "Cost of access code"
+msgstr "Preis des Zugangscodes"
+
+#: waeup/sirp/accesscodes/interfaces.py:45
+msgid "Workflow state"
+msgstr "Workflow-Status"
+
+#: waeup/sirp/accesscodes/interfaces.py:48
+msgid "Complete title of access code"
+msgstr "Vollständiger Titel des Zugangscodes"
+
+#: waeup/sirp/accesscodes/interfaces.py:51
+msgid "Purchaser"
+msgstr "Käufer"
+
+#: waeup/sirp/accesscodes/interfaces.py:54
+msgid "The history of access code as lines"
+msgstr "Die Historie eines Zugangscodes"
+
+#: waeup/sirp/accesscodes/interfaces.py:63
+msgid "Creation date"
+msgstr "Erstellungsdatum"
+
+#: waeup/sirp/accesscodes/interfaces.py:66
+msgid "Batch creator"
+msgstr "Erzeuger"
+
+#: waeup/sirp/accesscodes/interfaces.py:69
+msgid "Batch prefix"
+msgstr "Serien-Prefix"
+
+#: waeup/sirp/accesscodes/interfaces.py:72
+msgid "Batch number (1-3 digits)"
+msgstr "Seriennummer (1-3 Zeichen)"
+
+#: waeup/sirp/accesscodes/interfaces.py:76
+msgid "Number of access codes"
+msgstr "Zugangscode-Nummer"
+
+#: waeup/sirp/accesscodes/interfaces.py:84
+msgid "Number of disabled access codes inside the batch"
+msgstr "Anzahl der deaktivierten Zugangscodes"
+
+#: waeup/sirp/accesscodes/interfaces.py:89
+msgid "Number of used access codes inside the batch"
+msgstr "Anzahl der verbrauchten Zugangscodes"
+
+#: waeup/sirp/accesscodes/workflow.py:102
+msgid "Disable used PIN"
+msgstr "Benutzte PIN deaktivieren"
+
+#: waeup/sirp/accesscodes/workflow.py:109
+msgid "Reenable disabled PIN"
+msgstr "Deaktivierte PIN reaktivieren"
+
+#: waeup/sirp/accesscodes/workflow.py:37
+#: waeup/sirp/applicants/workflow.py:37
+msgid "initialized"
+msgstr "initialisiert"
+
+#: waeup/sirp/accesscodes/workflow.py:38
+msgid "used"
+msgstr "verwendet"
+
+#: waeup/sirp/accesscodes/workflow.py:81
+msgid "Initialize PIN"
+msgstr "PIN initialisieren"
+
+#: waeup/sirp/accesscodes/workflow.py:88
+msgid "Use PIN"
+msgstr "PIN verwenden"
+
+#: waeup/sirp/accesscodes/workflow.py:95
+msgid "Disable unused PIN"
+msgstr "Unbenutzte PIN deaktivieren"
+
+#: waeup/sirp/applicants/browser.py:117
+#: waeup/sirp/applicants/browser.py:325
+#: waeup/sirp/applicants/browser.py:745
+msgid "Could not delete:"
+msgstr "Konnte nicht gelöscht werden:"
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:120
+#: waeup/sirp/applicants/browser.py:328
+#: waeup/sirp/applicants/browser.py:748
+#: waeup/sirp/browser/pages.py:134
+#: waeup/sirp/students/browser.py:243
+#: waeup/sirp/students/browser.py:642
+#: waeup/sirp/students/browser.py:796
+#: waeup/sirp/students/browser.py:974
+#: waeup/sirp/students/browser.py:1184
+#: waeup/sirp/students/browser.py:1770
+msgid "Successfully removed: ${a}"
+msgstr "Erfolgreich entfernt: ${a}"
+
+#: waeup/sirp/applicants/browser.py:169
+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/sirp/applicants/browser.py:178
+msgid "Added:"
+msgstr "Hinzugefügt:"
+
+#: waeup/sirp/applicants/browser.py:190
+#: waeup/sirp/applicants/viewlets.py:44
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:12
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:36
+msgid "Applicants"
+msgstr "Antragsteller"
+
+#: waeup/sirp/applicants/browser.py:254
+#: waeup/sirp/applicants/browser.py:301
+#: waeup/sirp/applicants/browser.py:654
+#: waeup/sirp/applicants/browser.py:688
+#: waeup/sirp/applicants/browser.py:791
+#: waeup/sirp/applicants/browser.py:794
+#: waeup/sirp/applicants/browser.py:838
+#: waeup/sirp/browser/pages.py:476
+#: waeup/sirp/browser/pages.py:631
+#: waeup/sirp/browser/pages.py:654
+#: waeup/sirp/browser/pages.py:728
+#: waeup/sirp/browser/pages.py:1181
+#: waeup/sirp/browser/pages.py:1353
+#: waeup/sirp/browser/pages.py:1394
+#: waeup/sirp/browser/pages.py:1520
+#: waeup/sirp/browser/pages.py:1573
+#: waeup/sirp/browser/pages.py:1716
+#: waeup/sirp/browser/pages.py:1767
+#: waeup/sirp/browser/pages.py:1788
+#: waeup/sirp/hostels/browser.py:209
+#: waeup/sirp/hostels/browser.py:231
+#: waeup/sirp/hostels/browser.py:319
+#: waeup/sirp/students/browser.py:359
+#: waeup/sirp/students/browser.py:458
+#: waeup/sirp/students/browser.py:535
+#: waeup/sirp/students/browser.py:584
+#: waeup/sirp/students/browser.py:600
+#: waeup/sirp/students/browser.py:746
+#: waeup/sirp/students/browser.py:771
+#: waeup/sirp/students/browser.py:1444
+#: waeup/sirp/students/browser.py:1459
+#: waeup/sirp/students/browser.py:1574
+msgid "Save"
+msgstr "Speichern"
+
+#: waeup/sirp/applicants/browser.py:255
+#: waeup/sirp/applicants/browser.py:333
+#: waeup/sirp/applicants/browser.py:361
+msgid "Add applicant"
+msgstr "Antragsteller hinzufügen"
+
+#: waeup/sirp/applicants/browser.py:265
+#: waeup/sirp/applicants/viewlets.py:95
+msgid "Manage applicants container"
+msgstr "Ordner für Antragsteller verwalten"
+
+#: waeup/sirp/applicants/browser.py:305
+#: waeup/sirp/applicants/browser.py:717
+#: waeup/sirp/browser/pages.py:1397
+#: waeup/sirp/browser/pages.py:1719
+#: waeup/sirp/browser/pages.py:1791
+#: waeup/sirp/browser/pages.py:1903
+#: waeup/sirp/hostels/browser.py:54
+#: waeup/sirp/students/browser.py:80
+#: waeup/sirp/students/browser.py:385
+msgid "Form has been saved."
+msgstr "Das Formular wurde gespeichert."
+
+#: waeup/sirp/applicants/browser.py:314
+msgid "No applicant selected!"
+msgstr "Nichts ausgewählt!"
+
+#: waeup/sirp/applicants/browser.py:364
+msgid "Create application record"
+msgstr "Antrag hinzufügen"
+
+#: waeup/sirp/applicants/browser.py:369
+msgid "Applicant record created."
+msgstr "Antrag wurde erzeugt."
+
+#: waeup/sirp/applicants/browser.py:382
+msgid "Applicant"
+msgstr "Antragsteller"
+
+#: waeup/sirp/applicants/browser.py:397
+#: waeup/sirp/students/browser.py:285
+msgid "set"
+msgstr "gesetzt"
+
+#: waeup/sirp/applicants/browser.py:398
+#: waeup/sirp/students/browser.py:286
+msgid "unset"
+msgstr "ungesetzt"
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:403
+msgid "${a} Application Record ${b}"
+msgstr "${a} Antrag ${b}"
+
+#: waeup/sirp/applicants/browser.py:454
+msgid "Session configuration object is not available."
+msgstr "Session-Konfiguration ist nicht verfügbar."
+
+#: waeup/sirp/applicants/browser.py:461
+#: waeup/sirp/students/browser.py:1023
+msgid "This type of payment has already been made."
+msgstr "Diese Bezahlung wurde bereits ausgeführt."
+
+#: waeup/sirp/applicants/browser.py:474
+#: waeup/sirp/students/browser.py:1037
+msgid "Payment ticket created."
+msgstr "Bezahl-Ticket wurde erzeugt."
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:501
+#: waeup/sirp/students/browser.py:1056
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr "${a}: Online-Bezahl-Ticket ${b}"
+
+#: waeup/sirp/applicants/browser.py:529
+#: waeup/sirp/students/browser.py:1105
+msgid "Valid callback received."
+msgstr "Gültigen Rückruf erhalten."
+
+#: waeup/sirp/applicants/browser.py:550
+msgid "Payment Data"
+msgstr "Zahlung"
+
+#: waeup/sirp/applicants/browser.py:556
+msgid "Online Payment Receipt"
+msgstr "Quittung"
+
+#: waeup/sirp/applicants/browser.py:562
+msgid "Ticket not yet paid."
+msgstr "Ticket wurde noch nicht bezahlt."
+
+#: waeup/sirp/applicants/browser.py:627
+msgid "Uploaded image is too big!"
+msgstr "Das hochgeladene Bild ist zu groß!"
+
+#: waeup/sirp/applicants/browser.py:632
+msgid "jpg file extension expected."
+msgstr "jpg-Dateiendung erwartet."
+
+#: waeup/sirp/applicants/browser.py:64
+msgid "Application Section"
+msgstr "Antragstellung"
+
+#: waeup/sirp/applicants/browser.py:654
+#: waeup/sirp/applicants/browser.py:794
+#: waeup/sirp/applicants/browser.py:846
+msgid "Final Submit"
+msgstr "Endgültiges Einreichen"
+
+#: waeup/sirp/applicants/browser.py:655
+#: waeup/sirp/applicants/browser.py:758
+#: waeup/sirp/applicants/browser.py:792
+#: waeup/sirp/applicants/browser.py:827
+#: waeup/sirp/students/browser.py:980
+msgid "Add online payment ticket"
+msgstr "Ticket für Online-Bezahlung hinzufügen"
+
+#: waeup/sirp/applicants/browser.py:655
+#: waeup/sirp/applicants/browser.py:763
+#: waeup/sirp/applicants/browser.py:792
+#: waeup/sirp/applicants/browser.py:795
+#: waeup/sirp/applicants/browser.py:832
+#: waeup/sirp/students/browser.py:748
+#: waeup/sirp/students/browser.py:780
+#: waeup/sirp/students/browser.py:956
+#: waeup/sirp/students/browser.py:1752
+msgid "Remove selected tickets"
+msgstr "Ausgewählte Tickets löschen"
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:674
+msgid "${a} Application Form ${b}"
+msgstr "${a} Antragsformular ${b}"
+
+#: waeup/sirp/applicants/browser.py:685
+#: waeup/sirp/students/browser.py:356
+msgid "No transition"
+msgstr "Keine Änderung"
+
+#: waeup/sirp/applicants/browser.py:732
+#: waeup/sirp/students/browser.py:962
+msgid "No payment selected."
+msgstr "Nichts ausgewählt!"
+
+#: waeup/sirp/applicants/browser.py:76
+#: waeup/sirp/applicants/viewlets.py:88
+msgid "Manage application section"
+msgstr "Antragstellungsverfahren verwalten"
+
+#: waeup/sirp/applicants/browser.py:79
+#: waeup/sirp/applicants/browser.py:105
+#: waeup/sirp/applicants/browser.py:255
+#: waeup/sirp/applicants/browser.py:308
+#: waeup/sirp/browser/pages.py:634
+#: waeup/sirp/browser/pages.py:675
+#: waeup/sirp/browser/pages.py:1277
+#: waeup/sirp/browser/pages.py:1288
+#: waeup/sirp/browser/pages.py:1354
+#: waeup/sirp/browser/pages.py:1389
+#: waeup/sirp/hostels/browser.py:131
+#: waeup/sirp/students/browser.py:206
+#: waeup/sirp/students/browser.py:1151
+#: waeup/sirp/students/browser.py:1164
+msgid "Remove selected"
+msgstr "Ausgewählte Objekte löschen"
+
+#: waeup/sirp/applicants/browser.py:79
+#: waeup/sirp/applicants/browser.py:125
+#: waeup/sirp/applicants/browser.py:148
+#: waeup/sirp/applicants/browser.py:160
+msgid "Add applicants container"
+msgstr "Ordner für Antragsteller hinzufügen"
+
+#: waeup/sirp/applicants/browser.py:805
+#: waeup/sirp/students/browser.py:88
+msgid "The requested form is locked (read-only)."
+msgstr "Dieses Formular ist gesperrt."
+
+#: waeup/sirp/applicants/browser.py:81
+#: waeup/sirp/applicants/browser.py:139
+#: waeup/sirp/applicants/browser.py:256
+#: waeup/sirp/applicants/browser.py:347
+#: waeup/sirp/browser/pages.py:1355
+#: waeup/sirp/browser/pages.py:1414
+#: waeup/sirp/browser/pages.py:1524
+#: waeup/sirp/browser/pages.py:1610
+#: waeup/sirp/browser/pages.py:1770
+#: waeup/sirp/browser/pages.py:1827
+msgid "Remove selected local roles"
+msgstr "Ausgewählte lokale Rollen löschen"
+
+#: waeup/sirp/applicants/browser.py:819
+msgid "No passport picture uploaded."
+msgstr "Es wurde noch kein Passbild hochgeladen."
+
+#: waeup/sirp/applicants/browser.py:82
+#: waeup/sirp/applicants/browser.py:135
+#: waeup/sirp/applicants/browser.py:257
+#: waeup/sirp/applicants/browser.py:343
+#: waeup/sirp/browser/pages.py:1356
+#: waeup/sirp/browser/pages.py:1410
+#: waeup/sirp/browser/pages.py:1525
+#: waeup/sirp/browser/pages.py:1606
+#: waeup/sirp/browser/pages.py:1771
+#: waeup/sirp/browser/pages.py:1823
+msgid "Add local role"
+msgstr "Lokale Rolle hinzufügen"
+
+#: waeup/sirp/applicants/browser.py:821
+msgid "Passport picture confirmation box not ticked."
+msgstr "Bitte bestätigen Sie das Passbild."
+
+#: waeup/sirp/applicants/browser.py:83
+msgid "Applicants Containers"
+msgstr "Ordner für Antragsteller"
+
+#: waeup/sirp/applicants/browser.py:858
+#: waeup/sirp/students/browser.py:1619
+msgid "This form cannot be submitted. Wrong state!"
+msgstr "Dieses Formular kann nicht abgesendet werden, da Sie den falschen Antragsstatus haben."
+
+#: waeup/sirp/applicants/browser.py:863
+msgid "Form has been submitted."
+msgstr "Das Formular wurde eingereicht."
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:898
+msgid "Register for ${a} Application"
+msgstr "Antragstellung für ${a}"
+
+#: waeup/sirp/applicants/browser.py:904
+msgid "Application has not yet started."
+msgstr "Antragszeitraum hat noch nicht begonnen."
+
+#: waeup/sirp/applicants/browser.py:909
+msgid "Application has ended."
+msgstr "Antragszeitraum ist beendet."
+
+#: waeup/sirp/applicants/browser.py:918
+msgid "Get login credentials"
+msgstr "Zugangsdaten zusenden"
+
+#: waeup/sirp/applicants/browser.py:933
+msgid "You have successfully been registered for the"
+msgstr "Sie haben sich erfolgreich registriert für"
+
+#: waeup/sirp/applicants/browser.py:940
+msgid "Email could not been sent. Please retry later."
+msgstr "Email konnte nicht versendet werden. Bitte versuchen Sie es später noch einmal."
+
+#: waeup/sirp/applicants/browser.py:949
+msgid "Your registration was successful."
+msgstr "Ihre Registrierung war erfolgreich."
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:18
+msgid "Photograph"
+msgstr "Fotografie"
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:43
+msgid "Application Transition:"
+msgstr "Zustandsänderung der Antragstellung:"
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:59
+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/sirp/applicants/browser_templates/applicanteditpage.pt:74
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:43
+msgid "Acceptance Fee Payment Tickets"
+msgstr "Zullassungsgebühr-Tickets"
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:11
+msgid "Thanks for using SIRP!"
+msgstr "Vielen Dank, dass Sie SIRP verwendet haben!"
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:3
+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/sirp/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/sirp/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/sirp/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/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:42
+msgid "Application Number"
+msgstr "Antragsnummer"
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:46
+msgid "First Choice"
+msgstr "Erste Wahl"
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:48
+msgid "Application State"
+msgstr "Status der Antragstellung"
+
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:4
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:23
+msgid "Year"
+msgstr "Jahr"
+
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:7
+msgid "Application Period"
+msgstr "Beantragungsszeitraum"
+
+#: waeup/sirp/applicants/interfaces.py:197
+#: waeup/sirp/students/browser.py:713
+#: waeup/sirp/university/interfaces.py:38
+#: waeup/sirp/university/interfaces.py:66
+#: waeup/sirp/university/interfaces.py:86
+#: waeup/sirp/university/interfaces.py:117
+#: waeup/sirp/university/interfaces.py:139
+#: waeup/sirp/university/interfaces.py:179
+#: waeup/sirp/university/interfaces.py:192
+#: waeup/sirp/university/interfaces.py:241
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:39
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:68
+#: waeup/sirp/browser/templates/facultypage.pt:8
+#: waeup/sirp/browser/templates/departmentpage.pt:15
+#: waeup/sirp/browser/templates/departmentpage.pt:39
+#: waeup/sirp/browser/templates/searchpage.pt:13
+#: waeup/sirp/browser/templates/facultymanagepage.pt:41
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:36
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:13
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:34
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:5
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:25
+msgid "Code"
+msgstr "Kürzel"
+
+#: waeup/sirp/applicants/interfaces.py:204
+#: waeup/sirp/students/browser.py:714
+#: waeup/sirp/university/interfaces.py:199
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:41
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:70
+#: waeup/sirp/browser/templates/facultypage.pt:9
+#: waeup/sirp/browser/templates/departmentpage.pt:16
+#: waeup/sirp/browser/templates/departmentpage.pt:40
+#: waeup/sirp/browser/templates/searchpage.pt:14
+#: waeup/sirp/browser/templates/certificatepage.pt:30
+#: waeup/sirp/browser/templates/facultymanagepage.pt:43
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:32
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:37
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:35
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:6
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:27
+msgid "Title"
+msgstr "Titel"
+
+#: waeup/sirp/applicants/interfaces.py:211
+#: waeup/sirp/applicants/interfaces.py:324
+msgid "Application Target"
+msgstr "Antragsobjekt (Programm)"
+
+#: waeup/sirp/applicants/interfaces.py:219
+msgid "Entry Level"
+msgstr "Einstiegslevel"
+
+#: waeup/sirp/applicants/interfaces.py:226
+#: waeup/sirp/applicants/interfaces.py:332
+msgid "Year of Entrance"
+msgstr "Eingangsjahr"
+
+#: waeup/sirp/applicants/interfaces.py:234
+#: waeup/sirp/applicants/interfaces.py:340
+msgid "Applicants Container Type"
+msgstr "Ordnertyp"
+
+#: waeup/sirp/applicants/interfaces.py:250
+msgid "Category for the grouping of certificates"
+msgstr "Kategorie für die Gruppierung von Zertifikaten"
+
+#: waeup/sirp/applicants/interfaces.py:257
+msgid "Human readable description in reST format"
+msgstr "Lesbare Beschreibung im reST-Format"
+
+#: waeup/sirp/applicants/interfaces.py:273
+msgid "Application Start Date"
+msgstr "Beginn der Antragsperiode"
+
+#: waeup/sirp/applicants/interfaces.py:279
+msgid "Application Closing Date"
+msgstr "Ende der Antragsperiode"
+
+#: waeup/sirp/applicants/interfaces.py:285
+msgid "Forbid additions after deadline (enddate)"
+msgstr "Antragstellung nach Ablauf der Antragsfrist verbieten"
+
+#: waeup/sirp/applicants/interfaces.py:368
+msgid "Applicant Id"
+msgstr "Antragsteller Id"
+
+#: waeup/sirp/applicants/interfaces.py:373
+msgid "JAMB Registration Number"
+msgstr "JAMB Regsistrierungsnummer"
+
+#: waeup/sirp/applicants/interfaces.py:385
+#: waeup/sirp/students/interfaces.py:154
+msgid "First Name"
+msgstr "Vorname"
+
+#: waeup/sirp/applicants/interfaces.py:389
+#: waeup/sirp/students/interfaces.py:159
+msgid "Middle Name"
+msgstr "Zweiter Vorname"
+
+#: waeup/sirp/applicants/interfaces.py:393
+#: waeup/sirp/students/interfaces.py:164
+msgid "Last Name (Surname)"
+msgstr "Nachname"
+
+#: waeup/sirp/applicants/interfaces.py:397
+#: waeup/sirp/students/interfaces.py:213
+msgid "Date of Birth"
+msgstr "Geburtsdatum"
+
+#: waeup/sirp/applicants/interfaces.py:402
+msgid "State/LGA"
+msgstr "State/LGA"
+
+#: waeup/sirp/applicants/interfaces.py:407
+#: waeup/sirp/students/interfaces.py:169
+msgid "Sex"
+msgstr "Geschlecht"
+
+#: waeup/sirp/applicants/interfaces.py:413
+#: waeup/sirp/students/interfaces.py:490
+msgid "Email Address"
+msgstr "Emailadresse"
+
+#: waeup/sirp/applicants/interfaces.py:418
+#: waeup/sirp/students/interfaces.py:203
+msgid "Phone"
+msgstr "Telefon"
+
+#: waeup/sirp/applicants/interfaces.py:423
+#: waeup/sirp/applicants/interfaces.py:503
+msgid "1st Choice Course of Study"
+msgstr "Studienfach (1. Wahl)"
+
+#: waeup/sirp/applicants/interfaces.py:428
+#: waeup/sirp/applicants/interfaces.py:508
+msgid "2nd Choice Course of Study"
+msgstr "Studienfach (2. Wahl)"
+
+#: waeup/sirp/applicants/interfaces.py:437
+#: waeup/sirp/applicants/interfaces.py:513
+msgid "Screening Score"
+msgstr "Screening-Score"
+
+#: waeup/sirp/applicants/interfaces.py:441
+#: waeup/sirp/applicants/interfaces.py:518
+msgid "Screening Venue"
+msgstr "Screening-Ort"
+
+#: waeup/sirp/applicants/interfaces.py:445
+#: waeup/sirp/applicants/interfaces.py:523
+msgid "Admitted Course of Study"
+msgstr "Zugelassenes Studienfach"
+
+#: waeup/sirp/applicants/interfaces.py:451
+#: waeup/sirp/applicants/interfaces.py:530
+msgid "Notice"
+msgstr "Bemerkung"
+
+#: waeup/sirp/applicants/interfaces.py:473
+#: waeup/sirp/students/interfaces.py:149
+#: waeup/sirp/browser/templates/showstudentspage.pt:4
+#: waeup/sirp/students/browser_templates/containerpage.pt:29
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:35
+msgid "Student Id"
+msgstr "Studenten Id"
+
+#: waeup/sirp/applicants/interfaces.py:478
+msgid "Form locked"
+msgstr "Formular gesperrt"
+
+#: waeup/sirp/applicants/interfaces.py:557
+#: waeup/sirp/students/interfaces.py:469
+msgid "Payment Session"
+msgstr "Session der Zahlung"
+
+#: waeup/sirp/applicants/pdf.py:110
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:23
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr "Department:"
+
+#: waeup/sirp/applicants/pdf.py:121
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:15
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr "Fakultät:"
+
+#: waeup/sirp/applicants/pdf.py:182
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:26
+msgid "Admitted Course of Study:"
+msgstr "Zugelassen für Studienfach:"
+
+#. Default: ""
+#: waeup/sirp/applicants/pdf.py:200
+msgid "<font size=10>Proceed to the login page of the portal and enter your new credentials: user name= ${a}, password = ${b}.</font>"
+msgstr "<font size=10>Gehen Sie zur Login-Seite des Portals und geben Sie die neuen Zugansdaten ein: Benutzername= ${a}, Passwort = ${b}.</font>"
+
+#: waeup/sirp/applicants/pdf.py:206
+msgid "<font size=10>Change your password when you have  logged in.</font>"
+msgstr "<font size=10>Ändern Sie Ihr Passwort, sobald Sie sich eingeloggt haben.</font>"
+
+#: waeup/sirp/applicants/pdf.py:57
+msgid "Application Record"
+msgstr "Antrag"
+
+#: waeup/sirp/applicants/viewlets.py:103
+msgid "Register for application"
+msgstr "Für Antragstellung registrieren"
+
+#: waeup/sirp/applicants/viewlets.py:111
+msgid "View application record"
+msgstr "Antrag anschauen"
+
+#: waeup/sirp/applicants/viewlets.py:119
+msgid "Manage application record"
+msgstr "Antrag verwalten"
+
+#: waeup/sirp/applicants/viewlets.py:127
+msgid "Edit application record"
+msgstr "Antrag bearbeiten"
+
+#: waeup/sirp/applicants/viewlets.py:143
+msgid "Download application slip"
+msgstr "Antrag herunterladen"
+
+#: waeup/sirp/applicants/viewlets.py:151
+#: waeup/sirp/applicants/workflow.py:99
+#: waeup/sirp/students/browser.py:258
+msgid "Create student record"
+msgstr "Studenten erzeugen"
+
+#: waeup/sirp/applicants/viewlets.py:168
+#: waeup/sirp/students/viewlets.py:300
+msgid "Download payment receipt"
+msgstr "Quittung herunterladen"
+
+#: waeup/sirp/applicants/viewlets.py:183
+#: waeup/sirp/students/viewlets.py:315
+msgid "Request callback"
+msgstr "Bitte um Rückruf"
+
+#: waeup/sirp/applicants/viewlets.py:57
+msgid "Application"
+msgstr "Antragstellung"
+
+#: waeup/sirp/applicants/viewlets.py:73
+#: waeup/sirp/students/viewlets.py:512
+msgid "My Data"
+msgstr "Meine Daten"
+
+#: waeup/sirp/applicants/workflow.py:100
+#: waeup/sirp/students/workflow.py:25
+msgid "Student record created"
+msgstr "Student erzeugt"
+
+#: waeup/sirp/applicants/workflow.py:106
+#: waeup/sirp/applicants/workflow.py:113
+#: waeup/sirp/applicants/workflow.py:120
+#: waeup/sirp/applicants/workflow.py:127
+msgid "Reset application to started"
+msgstr "Antrag zurücksetzen"
+
+#: waeup/sirp/applicants/workflow.py:107
+#: waeup/sirp/applicants/workflow.py:114
+#: waeup/sirp/applicants/workflow.py:121
+#: waeup/sirp/applicants/workflow.py:128
+#: waeup/sirp/applicants/workflow.py:134
+msgid "Application reset"
+msgstr "Antrag zurückgesetzt"
+
+#: waeup/sirp/applicants/workflow.py:133
+msgid "Reset application to paid"
+msgstr "Antrag zurück auf 'Gebühren bezahlt' zurücksetzen"
+
+#: waeup/sirp/applicants/workflow.py:38
+msgid "started"
+msgstr "begonnen"
+
+#: waeup/sirp/applicants/workflow.py:39
+msgid "paid"
+msgstr "bezahlt"
+
+#: waeup/sirp/applicants/workflow.py:40
+msgid "submitted"
+msgstr "eingereicht"
+
+#: waeup/sirp/applicants/workflow.py:41
+#: waeup/sirp/interfaces.py:71
+msgid "admitted"
+msgstr "zugelassen"
+
+#: waeup/sirp/applicants/workflow.py:42
+msgid "not admitted"
+msgstr "nicht zugelassen"
+
+#: waeup/sirp/applicants/workflow.py:43
+#: waeup/sirp/interfaces.py:70
+msgid "created"
+msgstr "neu"
+
+#: waeup/sirp/applicants/workflow.py:49
+msgid "Initialize application"
+msgstr "Antrag initialisieren"
+
+#: waeup/sirp/applicants/workflow.py:52
+msgid "Application initialized"
+msgstr "Antrag initialisiert"
+
+#: waeup/sirp/applicants/workflow.py:57
+msgid "Start application"
+msgstr "Antragsvorgang starten"
+
+#: waeup/sirp/applicants/workflow.py:58
+msgid "Application started"
+msgstr "Antragsvorgang gestartet"
+
+#: waeup/sirp/applicants/workflow.py:64
+msgid "Pay acceptance fee"
+msgstr "Gebühr bezahlen"
+
+#: waeup/sirp/applicants/workflow.py:65
+msgid "Fee paid"
+msgstr "Gebühr bezahlt"
+
+#: waeup/sirp/applicants/workflow.py:71
+msgid "Submit application"
+msgstr "Antrag einreichen"
+
+#: waeup/sirp/applicants/workflow.py:72
+msgid "Application submitted"
+msgstr "Antrag eingereicht"
+
+#: waeup/sirp/applicants/workflow.py:78
+msgid "Admit applicant"
+msgstr "Antragsteller zulassen"
+
+#: waeup/sirp/applicants/workflow.py:79
+msgid "Applicant admitted"
+msgstr "Antragsteller zugelassen"
+
+#: waeup/sirp/applicants/workflow.py:85
+#: waeup/sirp/applicants/workflow.py:92
+msgid "Refuse application"
+msgstr "Antrag ablehnen"
+
+#: waeup/sirp/applicants/workflow.py:86
+#: waeup/sirp/applicants/workflow.py:93
+msgid "Application refused"
+msgstr "Antrag abgelehnt"
+
+#: waeup/sirp/browser/breadcrumbs.py:104
+msgid "Portal Settings"
+msgstr "Portaleinstellungen"
+
+#: waeup/sirp/browser/breadcrumbs.py:116
+#: waeup/sirp/browser/viewlets.py:615
+msgid "Academics"
+msgstr "Akademie"
+
+#: waeup/sirp/browser/breadcrumbs.py:123
+#: waeup/sirp/browser/pages.py:309
+msgid "Administration"
+msgstr "Verwaltung"
+
+#: waeup/sirp/browser/breadcrumbs.py:130
+#: waeup/sirp/browser/viewlets.py:360
+#: waeup/sirp/browser/viewlets.py:666
+msgid "Portal Configuration"
+msgstr "Portalkonfiguration"
+
+#: waeup/sirp/browser/breadcrumbs.py:148
+#: waeup/sirp/browser/pages.py:386
+#: waeup/sirp/browser/viewlets.py:375
+#: waeup/sirp/browser/viewlets.py:676
+msgid "Portal Users"
+msgstr "Benutzer"
+
+#: waeup/sirp/browser/breadcrumbs.py:155
+#: waeup/sirp/browser/pages.py:748
+#: waeup/sirp/browser/pages.py:1140
+#: waeup/sirp/browser/viewlets.py:393
+#: waeup/sirp/browser/viewlets.py:685
+msgid "Data Center"
+msgstr "Datenzentrum"
+
+#: waeup/sirp/browser/breadcrumbs.py:90
+msgid "Home"
+msgstr "Startseite"
+
+#: waeup/sirp/browser/pages.py:1016
+msgid "Edit headers or replace imported file!"
+msgstr "Bearbeiten Sie die Spaltenüberschrift oder tauschen Sie die Importdatei aus."
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:103
+msgid "Local role successfully removed: ${a}"
+msgstr "Lokale Rolle erfolgreich entfernt: ${a}"
+
+#: waeup/sirp/browser/pages.py:1063
+msgid "View processing log"
+msgstr "Logdatei betrachten"
+
+#: waeup/sirp/browser/pages.py:1064
+msgid "Back to data center"
+msgstr "Zurück zum Datenzentrum"
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1108
+msgid "Processing of ${a} rows failed."
+msgstr "Bearbeitung von ${a} Zeilen fehlgeschlagen."
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1110
+msgid "Successfully processed ${a} rows."
+msgstr "${a} Zeilen erfolgreich verarbeitet."
+
+#: waeup/sirp/browser/pages.py:1118
+msgid "Show logfiles"
+msgstr "Logdateien anzeigen"
+
+#: waeup/sirp/browser/pages.py:1120
+msgid "Back to Data Center"
+msgstr "Zurück zum Datenzentrum"
+
+#: waeup/sirp/browser/pages.py:1121
+msgid "Show"
+msgstr "Zeigen"
+
+#: waeup/sirp/browser/pages.py:113
+#: waeup/sirp/hostels/browser.py:256
+#: waeup/sirp/hostels/browser.py:287
+#: waeup/sirp/students/browser.py:230
+msgid "No item selected."
+msgstr "Nichts ausgewählt."
+
+#: waeup/sirp/browser/pages.py:1143
+msgid "Back"
+msgstr "Zurück"
+
+#: waeup/sirp/browser/pages.py:1151
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr "Logdateien können nur auf Unix-basierten Betriebsystem durchsucht werden."
+
+#: waeup/sirp/browser/pages.py:1179
+msgid "Edit data center settings"
+msgstr "Einstellungen des Datenzentrums bearbeiten"
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1199
+msgid "File already existed (not copied): ${a}"
+msgstr "Datei existiert (nicht kopiert): ${a}"
+
+#: waeup/sirp/browser/pages.py:1202
+msgid "Given storage path cannot be used."
+msgstr "Der Datenpfad kann nicht verwendet werden."
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1203
+msgid "Error: ${a}"
+msgstr "Fehler: ${a}"
+
+#: waeup/sirp/browser/pages.py:1206
+msgid "New storage path succefully set."
+msgstr "Neuen Datenpfad erfolgreich gesetzt."
+
+#: waeup/sirp/browser/pages.py:1265
+msgid "Academic Section"
+msgstr "Akademie"
+
+#: waeup/sirp/browser/pages.py:1277
+#: waeup/sirp/browser/pages.py:1293
+#: waeup/sirp/browser/pages.py:1310
+#: waeup/sirp/browser/pages.py:1314
+msgid "Add faculty"
+msgstr "Fakultät hinzufügen"
+
+#: waeup/sirp/browser/pages.py:1278
+msgid "Faculties"
+msgstr "Fakultäten"
+
+#: waeup/sirp/browser/pages.py:1282
+#: waeup/sirp/browser/viewlets.py:441
+msgid "Manage academic section"
+msgstr "Akademie verwalten"
+
+#: waeup/sirp/browser/pages.py:1321
+msgid "The faculty code chosen already exists."
+msgstr "Das Fakultätskürzel existiert bereits."
+
+#: waeup/sirp/browser/pages.py:1342
+#: waeup/sirp/browser/pages.py:1352
+msgid "Departments"
+msgstr "Departments"
+
+#: waeup/sirp/browser/pages.py:1354
+#: waeup/sirp/browser/pages.py:1405
+#: waeup/sirp/browser/pages.py:1424
+#: waeup/sirp/browser/pages.py:1428
+msgid "Add department"
+msgstr "Department hinzufügen"
+
+#: waeup/sirp/browser/pages.py:1362
+#: waeup/sirp/browser/viewlets.py:457
+msgid "Manage faculty"
+msgstr "Fakultät verwalten"
+
+#: waeup/sirp/browser/pages.py:1435
+msgid "The code chosen already exists in this faculty."
+msgstr "Das Kürzel existiert bereits in dieser Fakultät."
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1438
+msgid "Department ${a} added."
+msgstr "Department ${a} hinzugefügt."
+
+#: waeup/sirp/browser/pages.py:1455
+msgid "Courses and Certificates"
+msgstr "Kurse und Zertifikate"
+
+#: waeup/sirp/browser/pages.py:1485
+#: waeup/sirp/browser/pages.py:1504
+#: waeup/sirp/students/browser.py:96
+#: waeup/sirp/students/viewlets.py:472
+msgid "Students"
+msgstr "Studenten"
+
+#: waeup/sirp/browser/pages.py:152
+#: waeup/sirp/browser/viewlets.py:322
+#: waeup/sirp/browser/templates/staffsitelayout.pt:28
+#: waeup/sirp/browser/templates/studentsitelayout.pt:33
+msgid "Login"
+msgstr "Einloggen"
+
+#: waeup/sirp/browser/pages.py:1521
+#: waeup/sirp/browser/pages.py:1579
+msgid "Remove selected courses"
+msgstr "Ausgewählte Kurse löschen"
+
+#: waeup/sirp/browser/pages.py:1521
+#: waeup/sirp/browser/pages.py:1591
+#: waeup/sirp/browser/pages.py:1620
+#: waeup/sirp/browser/pages.py:1624
+msgid "Add course"
+msgstr "Kurs hinzufügen"
+
+#: waeup/sirp/browser/pages.py:1522
+#: waeup/sirp/browser/pages.py:1585
+msgid "Remove selected certificates"
+msgstr "Ausgewählte Zertifikate löschen"
+
+#: waeup/sirp/browser/pages.py:1522
+#: waeup/sirp/browser/pages.py:1596
+#: waeup/sirp/browser/pages.py:1657
+#: waeup/sirp/browser/pages.py:1661
+msgid "Add certificate"
+msgstr "Zertifikat hinzufügen"
+
+#: waeup/sirp/browser/pages.py:1531
+#: waeup/sirp/browser/viewlets.py:464
+msgid "Manage department"
+msgstr "Department verwalten"
+
+#: waeup/sirp/browser/pages.py:160
+msgid "You logged in."
+msgstr "Sie sind jetzt eingeloggt."
+
+#: waeup/sirp/browser/pages.py:1636
+msgid "A course with same code already exists: "
+msgstr "Ein Kurs mit gleichem Kürzel existiert bereits:"
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1642
+msgid "Course ${a} successfully created."
+msgstr "Kurs ${a} erfolgreich erzeugt."
+
+#: waeup/sirp/browser/pages.py:1673
+msgid "A certificate with same code already exists: "
+msgstr "Ein Zertifikat mit gleichem Kürzel existiert bereits:"
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1679
+msgid "Certificate ${a} successfully created."
+msgstr "Zertifikat ${a} erfolgreich erzeugt."
+
+#: waeup/sirp/browser/pages.py:1711
+#: waeup/sirp/browser/viewlets.py:481
+msgid "Edit course"
+msgstr "Kurs bearbeiten"
+
+#: waeup/sirp/browser/pages.py:1761
+msgid "Edit certificate"
+msgstr "Zertifikat bearbeiten"
+
+#: waeup/sirp/browser/pages.py:1768
+#: waeup/sirp/browser/pages.py:1799
+#: waeup/sirp/browser/pages.py:1840
+#: waeup/sirp/browser/pages.py:1842
+msgid "Add course referrer"
+msgstr "Kursreferenz hinzufügen"
+
+#: waeup/sirp/browser/pages.py:1769
+#: waeup/sirp/browser/pages.py:1794
+msgid "Remove selected course referrers"
+msgstr "Ausgewählte Kursreferenzen löschen"
+
+#: waeup/sirp/browser/pages.py:1775
+#: waeup/sirp/browser/viewlets.py:488
+msgid "Manage certificate"
+msgstr "Zertifikat verwalten"
+
+#: waeup/sirp/browser/pages.py:178
+msgid "You entered wrong credentials."
+msgstr "Sie haben falsche Zugangsdaten eingegeben."
+
+#: waeup/sirp/browser/pages.py:1847
+msgid "The chosen course referrer is already part of this certificate."
+msgstr "Die ausgewählte Kursreferenz ist bereits Teil des Zertifikates."
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1851
+msgid "Course referrer ${a}_${b} added."
+msgstr "Kursreferent ${a}_${b} hinzugefügt."
+
+#: waeup/sirp/browser/pages.py:1888
+#: waeup/sirp/browser/viewlets.py:505
+msgid "Edit course referrer"
+msgstr "Kursreferenz bearbeiten"
+
+#: waeup/sirp/browser/pages.py:1891
+msgid "Save and return"
+msgstr "Speichen und zurück"
+
+#: waeup/sirp/browser/pages.py:192
+msgid "You have been logged out. Thanks for using WAeUP SIRP!"
+msgstr "Sie wurden ausgeloggt. Danke dass Sie WAeUP Kofa benutzt haben."
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:228
+msgid "Contact ${a}"
+msgstr "Kontaktiere ${a}"
+
+#: waeup/sirp/browser/pages.py:234
+#: waeup/sirp/browser/pages.py:519
+msgid "Send message now"
+msgstr "Jetzt senden"
+
+#: waeup/sirp/browser/pages.py:253
+#: waeup/sirp/browser/pages.py:272
+#: waeup/sirp/browser/pages.py:536
+#: waeup/sirp/students/browser.py:320
+msgid "Your message has been sent."
+msgstr "Ihre Nachricht wurde verschickt."
+
+#: waeup/sirp/browser/pages.py:263
+msgid "Send now"
+msgstr "Sende jetzt"
+
+#: waeup/sirp/browser/pages.py:274
+msgid "A smtp server error occurred."
+msgstr "Es trat ein smtp-Serverfehler auf."
+
+#: waeup/sirp/browser/pages.py:299
+msgid "<h1>Welcome to WAeUP.SIRP</h1>"
+msgstr "<h1>Willkommen bei WAeUP.SIRP</h1>"
+
+#: waeup/sirp/browser/pages.py:387
+#: waeup/sirp/hostels/browser.py:195
+#: waeup/sirp/students/viewlets.py:163
+#: waeup/sirp/students/viewlets.py:171
+#: waeup/sirp/students/viewlets.py:226
+#: waeup/sirp/students/viewlets.py:243
+#: waeup/sirp/students/viewlets.py:283
+msgid "Manage"
+msgstr "Verwalten"
+
+#: waeup/sirp/browser/pages.py:388
+#: waeup/sirp/browser/viewlets.py:180
+msgid "Remove"
+msgstr "Entfernen"
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:395
+msgid "User account ${a} successfully deleted."
+msgstr "Benutzerkonto ${a} erfolgreich gelöscht."
+
+#: waeup/sirp/browser/pages.py:426
+#: waeup/sirp/browser/pages.py:428
+#: waeup/sirp/browser/viewlets.py:515
+msgid "Add user"
+msgstr "Benutzer hinzufügen"
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:450
+msgid "User account ${a} successfully added."
+msgstr "Benutzerkonto ${a} erfolgreich hinzugefügt."
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:469
+msgid "Edit user ${a}"
+msgstr "Benutzer bearbeiten"
+
+#: waeup/sirp/browser/pages.py:500
+msgid "User settings have been saved."
+msgstr "Benutzereinstellungen wurden gespeichert."
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:517
+#: waeup/sirp/students/browser.py:301
+msgid "Send message to ${a}"
+msgstr "Sende Nachricht an ${a}"
+
+#: waeup/sirp/browser/pages.py:546
+#: waeup/sirp/browser/viewlets.py:400
+msgid "My Preferences"
+msgstr "Meine Präferenzen"
+
+#: waeup/sirp/browser/pages.py:559
+#: waeup/sirp/browser/viewlets.py:415
+msgid "My Roles"
+msgstr "Meine Rollen"
+
+#: waeup/sirp/browser/pages.py:590
+msgid "Search Academic Section"
+msgstr "Akademie durchsuchen"
+
+#: waeup/sirp/browser/pages.py:617
+msgid "View portal configuration"
+msgstr "Portal-Konfiguration betrachten"
+
+#: waeup/sirp/browser/pages.py:630
+msgid "Edit portal configuration"
+msgstr "Portalkonfiguration bearbeiten"
+
+#: waeup/sirp/browser/pages.py:631
+#: waeup/sirp/browser/pages.py:680
+msgid "Update plugins"
+msgstr "Plugins aktualisieren"
+
+#: waeup/sirp/browser/pages.py:633
+#: waeup/sirp/browser/pages.py:661
+#: waeup/sirp/browser/pages.py:692
+msgid "Add session configuration"
+msgstr "Session-Konfiguration hinzufügen"
+
+#: waeup/sirp/browser/pages.py:658
+#: waeup/sirp/browser/pages.py:731
+msgid "Settings have been saved."
+msgstr "Einstellungen wurden gespeichert."
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:672
+msgid "Session ${a} Configuration"
+msgstr "Session ${a}-Konfiguration"
+
+#: waeup/sirp/browser/pages.py:683
+msgid "Plugins were updated. See log file for details."
+msgstr "Plugins wurden aktualisiert, siehe Logdatei."
+
+#: waeup/sirp/browser/pages.py:696
+msgid "Add Session Configuration"
+msgstr "Session-Konfiguration hinzufügen"
+
+#: waeup/sirp/browser/pages.py:703
+msgid "The session chosen already exists."
+msgstr "Die Session existiert bereits."
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:725
+msgid "Edit academic session ${a} configuration"
+msgstr "Session ${a}-Konfiguration bearbeiten"
+
+#: waeup/sirp/browser/pages.py:755
+msgid "Upload file"
+msgstr "Datei hochladen"
+
+#: waeup/sirp/browser/pages.py:757
+msgid "Upload"
+msgstr "Hochladen"
+
+#: waeup/sirp/browser/pages.py:803
+#: waeup/sirp/browser/pages.py:833
+#: waeup/sirp/browser/pages.py:953
+#: waeup/sirp/browser/pages.py:1061
+msgid "Process CSV file"
+msgstr "Verarbeite CSF-Datei"
+
+#: waeup/sirp/browser/pages.py:817
+#: waeup/sirp/browser/pages.py:918
+#: waeup/sirp/browser/pages.py:1037
+msgid "Import aborted."
+msgstr "Import abgebrochen."
+
+#: waeup/sirp/browser/pages.py:83
+msgid "No local role selected."
+msgstr "Keine lokalen Rollen ausgewählt."
+
+#: waeup/sirp/browser/pages.py:836
+msgid "Back to step 1"
+msgstr "Zurück zu Schritt 1"
+
+#: waeup/sirp/browser/pages.py:837
+msgid "Proceed to step 3"
+msgstr "Gehe zu Schritt 3"
+
+#: waeup/sirp/browser/pages.py:897
+msgid "Double headers: each column name may only appear once. "
+msgstr "Doppelte Spaltenüberschrift"
+
+#: waeup/sirp/browser/pages.py:905
+msgid "Replace imported file!"
+msgstr "Tauschen Sie die Importdatei aus!"
+
+#: waeup/sirp/browser/pages.py:930
+msgid "Update mode only!"
+msgstr "Nur Aktualisierungsmodus!"
+
+#: waeup/sirp/browser/pages.py:956
+#: waeup/sirp/browser/pages.py:1182
+msgid "Reset"
+msgstr "Zurücksetzen"
+
+#: waeup/sirp/browser/pages.py:957
+msgid "Set headerfields"
+msgstr "Spaltenüberschrift setzen"
+
+#: waeup/sirp/browser/pages.py:958
+msgid "Back to step 2"
+msgstr "Zurück zu Schritt 2"
+
+#: waeup/sirp/browser/pages.py:959
+msgid "Perform import"
+msgstr "Import durchführen"
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:10
+msgid "Course Code:"
+msgstr "Kurskürzel:"
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:14
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:7
+msgid "Course Title:"
+msgstr "Kursetitel:"
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:18
+msgid "Provided by:"
+msgstr "Angeboten von:"
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:26
+msgid "Level:"
+msgstr "Level:"
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:30
+msgid "Is mandatory course (not elective):"
+msgstr "Pflichtkurs:"
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:6
+msgid "Code:"
+msgstr "Kürzel:"
+
+#: waeup/sirp/browser/templates/certificatepage.pt:22
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:6
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:26
+msgid "Course Referrers"
+msgstr "Kursreferenzen"
+
+#: waeup/sirp/browser/templates/certificatepage.pt:28
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:31
+msgid "Semester"
+msgstr "Semester"
+
+#: waeup/sirp/browser/templates/certificatepage.pt:29
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:31
+msgid "Referrer"
+msgstr "Kursreferenz"
+
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr "Einstellungen"
+
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:8
+msgid "Session Configurations"
+msgstr "Session-Konfiguration"
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr "Schritt 1"
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr "Dateiname"
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:14
+msgid "Datasets"
+msgstr "Datensätze"
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr "Auswählen"
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr "Schritt 2"
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:43
+msgid "Mode:"
+msgstr "Modus:"
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr "Spaltenüberschriften in Ordnung"
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:12
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:6
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:4
+msgid "File:"
+msgstr "Datei:"
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:18
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:9
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:32
+msgid "Processor:"
+msgstr "Prozessor:"
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:24
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:13
+msgid "Processing mode:"
+msgstr "Verarbeitungsmodus:"
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:44
+msgid "change to:"
+msgstr "ändern zu:"
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr "Schritt 3"
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr "Ändern Sie die Spaltenüberschriften der Importdateien."
+
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr "Schritt 4"
+
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr "Stapelverarbeitung beendet."
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:12
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:16
+msgid "Date"
+msgstr "Datum"
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:14
+#: waeup/sirp/browser/templates/datacenterpage.pt:15
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr "Dateigröße"
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr "Zur Zeit sind keine Logdateien vorhanden."
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:12
+msgid "Overwrite contents in new location?"
+msgstr "Existierende Dateien an neuem Ort überschreiben?"
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:3
+#: waeup/sirp/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr "Pfad des Speicherorts:"
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:7
+msgid "Copy existing data to new storage?"
+msgstr "Existierende Dateien an neuen Ort kopieren?"
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterpage.pt:16
+msgid "Upload Date"
+msgstr "Hochladedatum"
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:10
+#: waeup/sirp/browser/templates/departmentpage.pt:3
+msgid "Courses"
+msgstr "Kurse"
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:101
+#: waeup/sirp/browser/templates/facultymanagepage.pt:75
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:92
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:93
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:60
+msgid "Local Role"
+msgstr "Lokale Rolle"
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:13
+#: waeup/sirp/browser/templates/departmentpage.pt:6
+msgid "Certificates"
+msgstr "Zertifikate"
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:16
+#: waeup/sirp/browser/templates/userscontainerpage.pt:7
+#: waeup/sirp/browser/templates/facultymanagepage.pt:16
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:7
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:17
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:11
+msgid "Local Roles"
+msgstr "Lokale Rollen"
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:6
+#: waeup/sirp/browser/templates/facultymanagepage.pt:6
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:5
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:7
+msgid "Settings"
+msgstr "Einstellungen"
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:97
+#: waeup/sirp/browser/templates/userscontainerpage.pt:4
+#: waeup/sirp/browser/templates/facultymanagepage.pt:71
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:88
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:89
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:56
+msgid "User Id"
+msgstr "Benutzer Id"
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:99
+#: waeup/sirp/browser/templates/userscontainerpage.pt:5
+#: waeup/sirp/browser/templates/facultymanagepage.pt:73
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:31
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:90
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:91
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:58
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:10
+#: waeup/sirp/hostels/browser_templates/containermanagepage.pt:11
+msgid "Name"
+msgstr "Name"
+
+#: waeup/sirp/browser/templates/facultypage.pt:1
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:1
+msgid "There no subobjects registered yet."
+msgstr "Es existieren bisher keine Unterobjekte."
+
+#: waeup/sirp/browser/templates/loginpage.pt:13
+#: waeup/sirp/browser/templates/usereditformpage.pt:8
+#: waeup/sirp/students/browser_templates/basepage.pt:14
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:17
+#: waeup/sirp/students/browser_templates/change_password.pt:10
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:31
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:34
+msgid "Password:"
+msgstr "Passwort:"
+
+#: waeup/sirp/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/sirp/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 Access Code (PWD) and inititialize your student account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/sirp/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: "Or simply forgot your student id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/sirp/browser/templates/loginpage.pt:44
+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/sirp/browser/templates/loginpage.pt:5
+msgid "User Name or Id:"
+msgstr "Benutzername oder Id:"
+
+#: waeup/sirp/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr "Meine portalweiten Rollen:"
+
+#: waeup/sirp/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr "Meine lokalen Rollen:"
+
+#: waeup/sirp/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/sirp/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/sirp/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr "Bitte beachten Sie:"
+
+#: waeup/sirp/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr "Sie haben möglicherweise die URL falsch eingetragen."
+
+#: waeup/sirp/browser/templates/searchpage.pt:15
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:36
+msgid "Type"
+msgstr "Typ"
+
+#: waeup/sirp/browser/templates/searchpage.pt:9
+#: waeup/sirp/students/browser_templates/containerpage.pt:21
+msgid "Search Results"
+msgstr "Suchergebnisse"
+
+#: waeup/sirp/browser/templates/showstudentspage.pt:5
+msgid "Fullname"
+msgstr "Vollständiger Name"
+
+#: waeup/sirp/browser/templates/showstudentspage.pt:6
+msgid "Status"
+msgstr "Status"
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:101
+#: waeup/sirp/browser/templates/studentsitelayout.pt:86
+msgid "Form Status:"
+msgstr "Formularstatus:"
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:113
+#: waeup/sirp/browser/templates/studentsitelayout.pt:99
+msgid "Back to top"
+msgstr "Zurück zum Anfang"
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:114
+#: waeup/sirp/browser/templates/studentsitelayout.pt:100
+msgid "Student Information and Registration Portal<br /> Copyright &copy; WAeUP Group, 2008-2012"
+msgstr "Studenten-Informations- und Registrierungsportal<br /> Copyright &copy; WAeUP Group, 2008-2012"
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:34
+#: waeup/sirp/browser/templates/studentsitelayout.pt:39
+msgid "Contact"
+msgstr "Kontakt"
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:40
+#: waeup/sirp/browser/templates/studentsitelayout.pt:45
+msgid "Logout"
+msgstr "Ausloggen"
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:60
+msgid "Student Id:"
+msgstr "Studenten Id:"
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:64
+msgid "State:"
+msgstr "Status:"
+
+#: waeup/sirp/browser/templates/usereditformpage.pt:14
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:23
+#: waeup/sirp/students/browser_templates/change_password.pt:16
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:37
+msgid "Retype password:"
+msgstr "Wiederhole Passwort:"
+
+#: waeup/sirp/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr "Portalweite Rollen:"
+
+#: waeup/sirp/browser/viewlets.py:170
+msgid "Add"
+msgstr "Hinzufügen"
+
+#: waeup/sirp/browser/viewlets.py:383
+#: waeup/sirp/students/viewlets.py:155
+msgid "Send email"
+msgstr "Email versenden"
+
+#: waeup/sirp/browser/viewlets.py:434
+msgid "Edit settings"
+msgstr "Einstellungen bearbeiten"
+
+#: waeup/sirp/browser/viewlets.py:448
+msgid "Search academic section"
+msgstr "Akademie durchsuchen"
+
+#: waeup/sirp/browser/viewlets.py:473
+#: waeup/sirp/browser/viewlets.py:497
+msgid "Show students"
+msgstr "Studenten zeigen"
+
+#: waeup/sirp/browser/viewlets.py:528
+#: waeup/sirp/browser/viewlets.py:536
+msgid "Show batch logs"
+msgstr "Logdateien zeigen"
+
+#: waeup/sirp/browser/viewlets.py:548
+msgid "Batch processing"
+msgstr "Stapelverarbeitung"
+
+#: waeup/sirp/browser/viewlets.py:557
+msgid "Upload CSV file"
+msgstr "Datei hochladen"
+
+#: waeup/sirp/browser/viewlets.py:630
+msgid "Enquiries"
+msgstr "Anfragen"
+
+#: waeup/sirp/browser/viewlets.py:91
+#: waeup/sirp/browser/viewlets.py:160
+#: waeup/sirp/students/viewlets.py:218
+#: waeup/sirp/students/viewlets.py:403
+msgid "Edit"
+msgstr "Bearbeiten"
+
+#: waeup/sirp/hostels/browser.py:105
+msgid "Accommodation Section"
+msgstr "Wohnheime"
+
+#: waeup/sirp/hostels/browser.py:113
+#: waeup/sirp/hostels/browser.py:123
+msgid "Manage accommodation section"
+msgstr "Wohnheime verwalten"
+
+#: waeup/sirp/hostels/browser.py:145
+#: waeup/sirp/hostels/browser.py:158
+msgid "Add hostel"
+msgstr "Wohnheim hinzufügen"
+
+#: waeup/sirp/hostels/browser.py:161
+msgid "Create hostel"
+msgstr "Wohnheim erzeugen"
+
+#: waeup/sirp/hostels/browser.py:170
+msgid "The hostel already exists."
+msgstr "Das Wohnheim existiert bereits."
+
+#: waeup/sirp/hostels/browser.py:172
+msgid "Hostel created."
+msgstr "Wohnheim erzeugt."
+
+#: waeup/sirp/hostels/browser.py:207
+msgid "Manage hostel"
+msgstr "Wohnheim verwalten"
+
+#: waeup/sirp/hostels/browser.py:210
+#: waeup/sirp/hostels/browser.py:236
+msgid "Update all beds"
+msgstr "Alle Betten aktualisieren"
+
+#: waeup/sirp/hostels/browser.py:211
+#: waeup/sirp/hostels/browser.py:250
+msgid "Switch reservation of selected beds"
+msgstr "Reservierung der ausgewählten Betten ändern"
+
+#: waeup/sirp/hostels/browser.py:212
+#: waeup/sirp/hostels/browser.py:281
+msgid "Release selected beds"
+msgstr "Ausgewählte Betten freigeben"
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:241
+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/sirp/hostels/browser.py:275
+msgid "Successfully switched beds: ${a}"
+msgstr "Erfolgreich geänderte Betten: ${a}"
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:299
+msgid "Successfully released beds: ${a}"
+msgstr "Erfolgreich freigegebene Betten: ${a}"
+
+#: waeup/sirp/hostels/browser.py:304
+msgid "No allocated bed selected."
+msgstr "Keine Betten ausgewählt."
+
+#: waeup/sirp/hostels/browser.py:316
+msgid "Allocate student"
+msgstr "Student zuweisen"
+
+#: waeup/sirp/hostels/browser.py:68
+#: waeup/sirp/hostels/browser.py:78
+msgid "Hostels"
+msgstr "Wohnheime"
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:95
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr "Block ${a}, Raum ${b}, Bett ${c}"
+
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:9
+#: waeup/sirp/hostels/browser_templates/containermanagepage.pt:9
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:35
+msgid "Id"
+msgstr "Id"
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:37
+msgid "Number"
+msgstr "Nummer"
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:56
+msgid "[allocate student]"
+msgstr "[Student zuweisen]"
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:6
+msgid "Hostel Data"
+msgstr "Wohnheimdaten"
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:9
+msgid "Beds"
+msgstr "Betten"
+
+#: waeup/sirp/hostels/hostel.py:180
+msgid "unreserved"
+msgstr "nicht reserviert"
+
+#: waeup/sirp/hostels/hostel.py:187
+msgid "reserved"
+msgstr "reserviert"
+
+#: waeup/sirp/hostels/interfaces.py:105
+msgid "Beds for Returning Students"
+msgstr "Betten für rückgemeldete Studenten"
+
+#: waeup/sirp/hostels/interfaces.py:112
+msgid "Beds for Final Year Students"
+msgstr "Betten für Studenten im letzten Studienjahr"
+
+#: waeup/sirp/hostels/interfaces.py:119
+msgid "Beds without category"
+msgstr "Unkategorisierte Betten"
+
+#: waeup/sirp/hostels/interfaces.py:126
+msgid "Special Handling"
+msgstr "Spezielle Handhabung"
+
+#: waeup/sirp/hostels/interfaces.py:137
+msgid "Female and male blocks overlap."
+msgstr "Blöcke für Männer und Frauen überlappen."
+
+#: waeup/sirp/hostels/interfaces.py:146
+msgid "Bed categories overlap."
+msgstr "Bettkategorien überlappen."
+
+#: waeup/sirp/hostels/interfaces.py:169
+msgid "Bed Id"
+msgstr "Betten Id"
+
+#: waeup/sirp/hostels/interfaces.py:175
+#: waeup/sirp/students/interfaces.py:427
+msgid "Bed Type"
+msgstr "Betttyp"
+
+#: waeup/sirp/hostels/interfaces.py:181
+msgid "Bed Number"
+msgstr "Bettnummer"
+
+#: waeup/sirp/hostels/interfaces.py:186
+#: waeup/sirp/hostels/interfaces.py:199
+msgid "Owner (Student)"
+msgstr "Eigentümer (Student)"
+
+#: waeup/sirp/hostels/interfaces.py:44
+msgid "Hostel Id"
+msgstr "Wohnheim Id"
+
+#: waeup/sirp/hostels/interfaces.py:49
+msgid "Sort Id"
+msgstr "Sortierungsnummer"
+
+#: waeup/sirp/hostels/interfaces.py:55
+msgid "Hostel Name"
+msgstr "Wohnheimname"
+
+#: waeup/sirp/hostels/interfaces.py:61
+msgid "Floors per Block"
+msgstr "Etagen pro Block"
+
+#: waeup/sirp/hostels/interfaces.py:67
+msgid "Rooms per Floor"
+msgstr "Räume pro Flur"
+
+#: waeup/sirp/hostels/interfaces.py:73
+msgid "Reserved Beds"
+msgstr "Reservierte Betten"
+
+#: waeup/sirp/hostels/interfaces.py:84
+msgid "Blocks for Female Students"
+msgstr "Blöcke für Frauen"
+
+#: waeup/sirp/hostels/interfaces.py:91
+msgid "Blocks for Male Students"
+msgstr "Blöcke für Männer"
+
+#: waeup/sirp/hostels/interfaces.py:98
+msgid "Beds for Fresh Students"
+msgstr "Betten für Erstsemester"
+
+#: waeup/sirp/hostels/vocabularies.py:57
+msgid "Bed A"
+msgstr "Bett A"
+
+#: waeup/sirp/hostels/vocabularies.py:58
+msgid "Bed B"
+msgstr "Bett B"
+
+#: waeup/sirp/hostels/vocabularies.py:59
+msgid "Bed C"
+msgstr "Bett C"
+
+#: waeup/sirp/hostels/vocabularies.py:60
+msgid "Bed D"
+msgstr "Bett D"
+
+#: waeup/sirp/hostels/vocabularies.py:61
+msgid "Bed E"
+msgstr "Bett E"
+
+#: waeup/sirp/hostels/vocabularies.py:62
+msgid "Bed F"
+msgstr "Bett F"
+
+#: waeup/sirp/hostels/vocabularies.py:63
+msgid "Bed G"
+msgstr "Bett G"
+
+#: waeup/sirp/hostels/vocabularies.py:64
+msgid "Bed H"
+msgstr "Bett H"
+
+#: waeup/sirp/hostels/vocabularies.py:65
+msgid "Bed I"
+msgstr "Bett I"
+
+#: waeup/sirp/hostels/vocabularies.py:69
+msgid "Block A"
+msgstr "Block A"
+
+#: waeup/sirp/hostels/vocabularies.py:70
+msgid "Block B"
+msgstr "Block B"
+
+#: waeup/sirp/hostels/vocabularies.py:71
+msgid "Block C"
+msgstr "Block C"
+
+#: waeup/sirp/hostels/vocabularies.py:72
+msgid "Block D"
+msgstr "Block D"
+
+#: waeup/sirp/hostels/vocabularies.py:73
+msgid "Block E"
+msgstr "Block E"
+
+#: waeup/sirp/hostels/vocabularies.py:74
+msgid "Block F"
+msgstr "Block F"
+
+#: waeup/sirp/hostels/vocabularies.py:75
+msgid "Block G"
+msgstr "Block G"
+
+#: waeup/sirp/hostels/vocabularies.py:76
+msgid "Block H"
+msgstr "Block H"
+
+#: waeup/sirp/hostels/vocabularies.py:77
+msgid "Block I"
+msgstr "Block I"
+
+#: waeup/sirp/hostels/vocabularies.py:78
+msgid "Block K"
+msgstr "Block K"
+
+#: waeup/sirp/hostels/vocabularies.py:79
+msgid "Block L"
+msgstr "Block L"
+
+#: waeup/sirp/hostels/vocabularies.py:80
+msgid "Block M"
+msgstr "Block M"
+
+#: waeup/sirp/hostels/vocabularies.py:81
+msgid "Block N"
+msgstr "Block N"
+
+#: waeup/sirp/hostels/vocabularies.py:82
+msgid "Block O"
+msgstr "Block O"
+
+#: waeup/sirp/hostels/vocabularies.py:83
+msgid "Block P"
+msgstr "Block P"
+
+#: waeup/sirp/hostels/vocabularies.py:87
+msgid "Regular Hostel"
+msgstr "Standard-Wohnheim"
+
+#: waeup/sirp/hostels/vocabularies.py:88
+msgid "Blocked Hostel"
+msgstr "Gesperrtes Wohnheim"
+
+#: waeup/sirp/hostels/vocabularies.py:89
+msgid "Postgraduate Hostel"
+msgstr "Postgraduierten-Wohnheim"
+
+#: waeup/sirp/interfaces.py:72
+msgid "clearance started"
+msgstr "Einschreibung begonnen"
+
+#: waeup/sirp/interfaces.py:73
+msgid "clearance requested"
+msgstr "Einschreibung beantragt"
+
+#: waeup/sirp/interfaces.py:74
+msgid "cleared"
+msgstr "eingeschrieben"
+
+#: waeup/sirp/interfaces.py:75
+msgid "school fee paid"
+msgstr "Gebühren bezahlt"
+
+#: waeup/sirp/interfaces.py:76
+msgid "returning"
+msgstr "rückgemeldet"
+
+#: waeup/sirp/interfaces.py:77
+msgid "courses registered"
+msgstr "Kurse registriert"
+
+#: waeup/sirp/interfaces.py:78
+msgid "courses validated"
+msgstr "Kurse validiert"
+
+#: waeup/sirp/objecthistory.py:73
+msgid "by"
+msgstr "von"
+
+#: waeup/sirp/payments/interfaces.py:106
+msgid "Surcharge 1"
+msgstr "Zusatzgebühr 1"
+
+#: waeup/sirp/payments/interfaces.py:113
+msgid "Surcharge 2"
+msgstr "Zusatzgebühr 2"
+
+#: waeup/sirp/payments/interfaces.py:120
+msgid "Surcharge 3"
+msgstr "Zusatzgebühr 3"
+
+#: waeup/sirp/payments/interfaces.py:133
+msgid "Activation Code"
+msgstr "Aktivierungs-Code"
+
+#: waeup/sirp/payments/interfaces.py:140
+msgid "Response Amount Approved"
+msgstr "Response Amount Approved"
+
+#: waeup/sirp/payments/interfaces.py:147
+msgid "Response Description"
+msgstr "Response Description"
+
+#: waeup/sirp/payments/interfaces.py:154
+msgid "Response Payment Reference"
+msgstr "Response Payment Reference"
+
+#: waeup/sirp/payments/interfaces.py:161
+msgid "Response Code"
+msgstr "Response Code"
+
+#: waeup/sirp/payments/interfaces.py:168
+msgid "Response Card Number"
+msgstr "Response Card Number"
+
+#: waeup/sirp/payments/interfaces.py:24
+msgid "Not yet paid"
+msgstr "Noch nicht bezahlt"
+
+#: waeup/sirp/payments/interfaces.py:25
+msgid "Paid"
+msgstr "Bezahlt"
+
+#: waeup/sirp/payments/interfaces.py:26
+msgid "Failed"
+msgstr "Fehler"
+
+#: waeup/sirp/payments/interfaces.py:30
+msgid "School Fee"
+msgstr "Schulgebühr"
+
+#: waeup/sirp/payments/interfaces.py:31
+msgid "Clearance"
+msgstr "Einschreibung"
+
+#: waeup/sirp/payments/interfaces.py:32
+msgid "Bed Allocation"
+msgstr "Bettenzuweisung"
+
+#: waeup/sirp/payments/interfaces.py:33
+msgid "Hostel Maintenance"
+msgstr "Wohnraum-Mietgebühr"
+
+#: waeup/sirp/payments/interfaces.py:34
+msgid "Transfer"
+msgstr "Studienfachwechsel"
+
+#: waeup/sirp/payments/interfaces.py:35
+msgid "Gown"
+msgstr "Talar"
+
+#: waeup/sirp/payments/interfaces.py:36
+msgid "Acceptance Fee"
+msgstr "Zulassungsgebühr"
+
+#: waeup/sirp/payments/interfaces.py:51
+msgid "Payment Category"
+msgstr "Kategorie der Zahlung"
+
+#: waeup/sirp/payments/interfaces.py:58
+msgid "Payment Item"
+msgstr "Gegenstand der Zahlung"
+
+#: waeup/sirp/payments/interfaces.py:64
+msgid "Payment State"
+msgstr "Status der Zahlung"
+
+#: waeup/sirp/payments/interfaces.py:71
+msgid "Ticket Creation Date"
+msgstr "Erstellungsdatum"
+
+#: waeup/sirp/payments/interfaces.py:76
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:10
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:84
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "Payment Date"
+msgstr "Datum der Zahlung"
+
+#: waeup/sirp/payments/interfaces.py:82
+msgid "Amount Authorized"
+msgstr "Genehmigter Betrag"
+
+#: waeup/sirp/payments/interfaces.py:94
+msgid "Payment Access Code"
+msgstr "Zugangscode der Zahlung"
+
+#: waeup/sirp/students/browser.py:1003
+#: waeup/sirp/students/browser.py:1225
+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/sirp/students/browser.py:1072
+msgid "This ticket has already been paid."
+msgstr "Diese Bezahlung wurde bereits ausgeführt."
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1085
+#: waeup/sirp/students/browser.py:1093
+#: waeup/sirp/students/browser.py:1101
+msgid "Valid callback received. ${a}"
+msgstr "Gültigen Rückruf erhalten. ${a}"
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1155
+msgid "${a}: Accommodation"
+msgstr " ${a}: Unterkunft"
+
+#: waeup/sirp/students/browser.py:1167
+msgid "You are not allowed to remove bed tickets."
+msgstr "Ihnen ist nicht erlaubt, Betttickets zu entfernen."
+
+#: waeup/sirp/students/browser.py:117
+#: waeup/sirp/students/viewlets.py:114
+#: waeup/sirp/students/viewlets.py:534
+msgid "Study Course"
+msgstr "Studiengang"
+
+#: waeup/sirp/students/browser.py:1174
+msgid "No bed ticket selected."
+msgstr "Kein Ticket ausgewählt."
+
+#: waeup/sirp/students/browser.py:1205
+msgid "Add bed ticket"
+msgstr "Bettticket hinzufügen"
+
+#: waeup/sirp/students/browser.py:1207
+msgid "Create bed ticket"
+msgstr "Bettticket erzeugen"
+
+#: waeup/sirp/students/browser.py:1215
+msgid "Your data are incomplete."
+msgstr "Die Daten sind unvollständig."
+
+#: waeup/sirp/students/browser.py:1219
+msgid "You are in the wrong registration state."
+msgstr "Sie haben den falschen Status."
+
+#: waeup/sirp/students/browser.py:123
+#: waeup/sirp/students/viewlets.py:120
+#: waeup/sirp/students/viewlets.py:535
+msgid "Payments"
+msgstr "Zahlungen"
+
+#: waeup/sirp/students/browser.py:1230
+msgid "You already booked a bed space in current accommodation session."
+msgstr "Sie haben in der aktuellen Session bereits ein Bett gebucht."
+
+#: waeup/sirp/students/browser.py:1241
+#: waeup/sirp/students/browser.py:1537
+#: waeup/sirp/students/browser.py:1650
+msgid "Activation code is invalid."
+msgstr "Aktivierungs-Code ist ungültig."
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1261
+#: waeup/sirp/students/browser.py:1389
+msgid "There is no free bed in your category ${a}."
+msgstr "In der Kategorie ${a} ist kein Bett mehr frei."
+
+#: waeup/sirp/students/browser.py:1266
+#: waeup/sirp/students/browser.py:1542
+#: waeup/sirp/students/browser.py:1655
+msgid "Activation code has already been used."
+msgstr "Der Aktivierungs-Code wurde bereits verwendet."
+
+#: waeup/sirp/students/browser.py:1269
+#: waeup/sirp/students/browser.py:1545
+#: waeup/sirp/students/browser.py:1658
+#: waeup/sirp/students/browser.py:1917
+msgid "invalidated"
+msgstr "entwertet"
+
+#: waeup/sirp/students/browser.py:1274
+#: waeup/sirp/students/browser.py:1549
+#: waeup/sirp/students/browser.py:1663
+msgid "You are not the owner of this access code."
+msgstr "Sie sind nicht der Eigentümer dieses Zugangscodes."
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1285
+#: waeup/sirp/students/browser.py:1403
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr "${a}, Block ${b}, Raum ${c}, Bett ${d} (${e})"
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1294
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr "Bettticket erzeugt und Bett gebucht: ${a}"
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1312
+msgid "Bed Ticket for Session ${a}"
+msgstr "Bett-Ticket für Session ${a}"
+
+#: waeup/sirp/students/browser.py:1328
+msgid "Bed Allocation Data"
+msgstr "Bettenzuweisungsdaten"
+
+#: waeup/sirp/students/browser.py:1334
+msgid "Bed Allocation: "
+msgstr "Bettenzuweisung"
+
+#: waeup/sirp/students/browser.py:1361
+msgid "Students in reserved beds can't be relocated."
+msgstr "Studenten in reservierten Betten können nicht umgezogen werden."
+
+#: waeup/sirp/students/browser.py:1367
+msgid "Student can't be relocated."
+msgstr "Studen konnte nicht umgezogen werden."
+
+#: waeup/sirp/students/browser.py:138
+#: waeup/sirp/students/viewlets.py:126
+msgid "Accommodation"
+msgstr "Unterkunft"
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1410
+msgid "Student relocated: ${a}"
+msgstr "Student umgezogen: ${a}"
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1429
+msgid "${a}: History"
+msgstr "${a}: Historie"
+
+#: waeup/sirp/students/browser.py:1441
+#: waeup/sirp/students/viewlets.py:356
+msgid "Edit base data"
+msgstr "Basisdaten verwalten"
+
+#: waeup/sirp/students/browser.py:1456
+#: waeup/sirp/students/viewlets.py:365
+msgid "Change password"
+msgstr "Passwort ändern"
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:147
+msgid "Bed Ticket ${a}"
+msgstr "Bett-Ticket ${a}"
+
+#: waeup/sirp/students/browser.py:1470
+msgid "Password changed."
+msgstr "Passwort geändert"
+
+#: waeup/sirp/students/browser.py:1482
+msgid "Upload portrait"
+msgstr "Portrait hochladen"
+
+#: waeup/sirp/students/browser.py:1497
+#: waeup/sirp/students/viewlets.py:389
+#: waeup/sirp/students/workflow.py:44
+msgid "Start clearance"
+msgstr "Einschreibung starten"
+
+#: waeup/sirp/students/browser.py:1501
+msgid "Start clearance now"
+msgstr "Einschreibung jetzt starten"
+
+#: waeup/sirp/students/browser.py:1518
+#: waeup/sirp/students/browser.py:1639
+msgid "Wrong state"
+msgstr "Falscher Status"
+
+#: waeup/sirp/students/browser.py:1522
+msgid "No portrait uploaded."
+msgstr "Es wurde kein Passbild hochgeladen."
+
+#: waeup/sirp/students/browser.py:1526
+msgid "Not all required fields filled."
+msgstr "Es wurden nicht alle Pflichtfelder ausgefüllt."
+
+#: waeup/sirp/students/browser.py:1553
+msgid "Clearance process has been started."
+msgstr "Einschreibung wurde gestartet"
+
+#: waeup/sirp/students/browser.py:1565
+msgid "Edit clearance data"
+msgstr "Bearbeite Einschreibungsdaten"
+
+#: waeup/sirp/students/browser.py:1577
+#: waeup/sirp/students/browser.py:1591
+msgid "Clearance form has been saved."
+msgstr "Einschreibungsdaten wurden gespeichert."
+
+#: waeup/sirp/students/browser.py:1585
+msgid "Save and request clearance"
+msgstr "Speichern und Einschreibung beantragen"
+
+#: waeup/sirp/students/browser.py:1600
+#: waeup/sirp/students/workflow.py:58
+msgid "Request clearance"
+msgstr "Einschreibung beantragen"
+
+#: waeup/sirp/students/browser.py:1601
+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/sirp/students/browser.py:1604
+msgid "Request clearance now"
+msgstr "Einschreibung beantragen"
+
+#: waeup/sirp/students/browser.py:1613
+msgid "This isn't your CLR access code."
+msgstr "Dies ist nicht Ihr CLR-Zugangscode"
+
+#: waeup/sirp/students/browser.py:1622
+msgid "Clearance has been requested."
+msgstr "Einschreibung wurde beantragt."
+
+#: waeup/sirp/students/browser.py:1631
+#: waeup/sirp/students/viewlets.py:418
+msgid "Start course registration"
+msgstr "Kurseregistrierung starten"
+
+#: waeup/sirp/students/browser.py:1635
+msgid "Start course registration now"
+msgstr "Kursregistrierung jetzt starten"
+
+#: waeup/sirp/students/browser.py:1671
+msgid "Course registration has been started."
+msgstr "Kursregistrierung wurde gestartet."
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1690
+msgid "Add current level ${a}"
+msgstr "Aktuelles Level ${a} hinzufügen"
+
+#: waeup/sirp/students/browser.py:1699
+msgid "Create course list now"
+msgstr "Kursliste jetzt erzeugen"
+
+#: waeup/sirp/students/browser.py:170
+msgid "Student Section"
+msgstr "Studenten"
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1738
+msgid "Add and remove course tickets of study level ${a}"
+msgstr "Kurstickets des Levels  ${a} hinzufügen oder löschen"
+
+#: waeup/sirp/students/browser.py:1775
+msgid "Register course list"
+msgstr "Registriere Kursliste"
+
+#: waeup/sirp/students/browser.py:1779
+msgid "Course list has been registered."
+msgstr "Kurseliste wurde registriert."
+
+#: waeup/sirp/students/browser.py:1825
+msgid "Change my password"
+msgstr "Passwort ändern"
+
+#: waeup/sirp/students/browser.py:1835
+msgid "Get new login credentials"
+msgstr "Zugangsdaten zusenden"
+
+#: waeup/sirp/students/browser.py:1849
+msgid "No student record found."
+msgstr "Keinen Studenten gefunden."
+
+#: waeup/sirp/students/browser.py:1857
+msgid "You have successfully changed your password for the"
+msgstr "Sie haben erfolgreich Ihr Passwort geändert für das"
+
+#: waeup/sirp/students/browser.py:186
+#: waeup/sirp/students/browser.py:222
+msgid "Empty search string"
+msgstr "Leerer Suchtext"
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1862
+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/sirp/students/browser.py:1873
+msgid "Set password for first-time login"
+msgstr "Passwort setzen"
+
+#: waeup/sirp/students/browser.py:1876
+msgid "Set"
+msgstr "Setzen"
+
+#: waeup/sirp/students/browser.py:1891
+msgid "More than one student found."
+msgstr "Mehr als einen Studenten gefunden."
+
+#: waeup/sirp/students/browser.py:1899
+msgid "Access code is invalid."
+msgstr "Aktivierungs-Code ist ungültig."
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1902
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr "Passwort wurde bereits gesetzt. Ihre Studenten Id ist ${a}"
+
+#: waeup/sirp/students/browser.py:1908
+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/sirp/students/browser.py:1914
+msgid "Access code has already been used."
+msgstr "Der Aktivierungs-Code wurde bereits verwendet."
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1923
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr "Passwort wurde gesetzt. Ihre Studenten Id ist ${a}"
+
+#: waeup/sirp/students/browser.py:204
+#: waeup/sirp/students/viewlets.py:139
+msgid "Manage student section"
+msgstr "Studenten verwalten"
+
+#: waeup/sirp/students/browser.py:228
+#: waeup/sirp/students/browser.py:1888
+msgid "No student found."
+msgstr "Keinen Studenten gefunden."
+
+#: waeup/sirp/students/browser.py:255
+#: waeup/sirp/students/viewlets.py:146
+msgid "Add student"
+msgstr "Student hinzufügen"
+
+#: waeup/sirp/students/browser.py:263
+msgid "Student record created."
+msgstr "Student erzeugt."
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:279
+msgid "${a}: Base Data"
+msgstr "${a}: Basisdaten"
+
+#: waeup/sirp/students/browser.py:322
+#: waeup/sirp/students/browser.py:1865
+msgid "An smtp server error occurred."
+msgstr "Es trat ein smtp-Serverfehler auf."
+
+#: waeup/sirp/students/browser.py:333
+msgid "Manage base data"
+msgstr "Basisdaten verwalten"
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:402
+msgid "${a}: Clearance Data"
+msgstr "${a}: Einschreibungsdaten"
+
+#: waeup/sirp/students/browser.py:418
+#: waeup/sirp/students/viewlets.py:104
+#: waeup/sirp/students/viewlets.py:532
+#: waeup/sirp/students/browser_templates/clearanceeditpage.pt:6
+msgid "Clearance Data"
+msgstr "Einschreibungsdaten"
+
+#: waeup/sirp/students/browser.py:424
+msgid "Clearance Slip of "
+msgstr "Einschreibungsbeleg für"
+
+#: waeup/sirp/students/browser.py:444
+msgid "Manage clearance data"
+msgstr "Verwalte Einschreibungsdaten"
+
+#: waeup/sirp/students/browser.py:473
+msgid "Student has been cleared."
+msgstr "Student wurde eingeschrieben."
+
+#: waeup/sirp/students/browser.py:475
+#: waeup/sirp/students/browser.py:499
+msgid "Student is in wrong state."
+msgstr "Student hat den falschen Status."
+
+#: waeup/sirp/students/browser.py:492
+msgid "Clearance has been annulled."
+msgstr "Einschreibung wurde annulliert."
+
+#: waeup/sirp/students/browser.py:496
+msgid "Clearance request has been rejected."
+msgstr "Einschreibung wurde abgelehnt."
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:522
+msgid "${a}: Personal Data"
+msgstr "${a}: Persönliche Daten"
+
+#: waeup/sirp/students/browser.py:532
+msgid "Manage personal data"
+msgstr "Persönliche Daten verwalten"
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:552
+msgid "${a}: Study Course"
+msgstr "${a}: Studiengang"
+
+#: waeup/sirp/students/browser.py:582
+msgid "Manage study course"
+msgstr "Studiengang verwalten"
+
+#: waeup/sirp/students/browser.py:585
+#: waeup/sirp/students/browser.py:626
+msgid "Remove selected levels"
+msgstr "Ausgewählte Levels  löschen"
+
+#: waeup/sirp/students/browser.py:586
+#: waeup/sirp/students/browser.py:612
+msgid "Add study level"
+msgstr "Level  hinzufügen"
+
+#: waeup/sirp/students/browser.py:620
+msgid "Study level has been added."
+msgstr "Level wurde hinzugefügt."
+
+#: waeup/sirp/students/browser.py:622
+#: waeup/sirp/students/browser.py:1708
+msgid "This level exists."
+msgstr "Dieses Level existiert."
+
+#: waeup/sirp/students/browser.py:632
+msgid "No study level selected."
+msgstr "Kein Level ausgewählt."
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:668
+msgid "${a}: Study Level ${b}"
+msgstr "${a}: Level ${b}"
+
+#: waeup/sirp/students/browser.py:691
+msgid "Level Data"
+msgstr "Leveldaten"
+
+#: waeup/sirp/students/browser.py:697
+msgid "Course List"
+msgstr "Kursliste"
+
+#: waeup/sirp/students/browser.py:706
+msgid "Course Registration Slip  "
+msgstr "Kursregistrierungsbeleg"
+
+#: waeup/sirp/students/browser.py:712
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:35
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:12
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:33
+msgid "Sem."
+msgstr "Sem."
+
+#: waeup/sirp/students/browser.py:715
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:38
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:36
+msgid "Dept."
+msgstr "Dept."
+
+#: waeup/sirp/students/browser.py:716
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:16
+msgid "Faculty"
+msgstr "Fakultät"
+
+#: waeup/sirp/students/browser.py:717
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:40
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:38
+msgid "Cred."
+msgstr "Cred."
+
+#: waeup/sirp/students/browser.py:718
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:39
+msgid "Mand."
+msgstr "Pflicht"
+
+#: waeup/sirp/students/browser.py:719
+#: waeup/sirp/students/interfaces.py:370
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:40
+msgid "Score"
+msgstr "Score"
+
+#: waeup/sirp/students/browser.py:747
+#: waeup/sirp/students/browser.py:776
+#: waeup/sirp/students/browser.py:861
+#: waeup/sirp/students/browser.py:866
+#: waeup/sirp/students/browser.py:1748
+#: waeup/sirp/students/browser.py:1798
+msgid "Add course ticket"
+msgstr "Kursticket hinzufügen"
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:768
+msgid "Manage study level ${a}"
+msgstr "Verwalte Level  ${a}"
+
+#: waeup/sirp/students/browser.py:786
+#: waeup/sirp/students/browser.py:1758
+msgid "No ticket selected."
+msgstr "Kein Ticket ausgewählt."
+
+#: waeup/sirp/students/browser.py:810
+#: waeup/sirp/students/browser.py:832
+msgid "This level does not correspond current level."
+msgstr "Dieses Level stimmt nicht mit dem aktuellen Studienjahr des Studenten überein."
+
+#: waeup/sirp/students/browser.py:814
+msgid "Course list has been validated."
+msgstr "Kurseliste wurde validiert."
+
+#: waeup/sirp/students/browser.py:816
+#: waeup/sirp/students/browser.py:844
+msgid "Student is in the wrong state."
+msgstr "Student hat den falschen Status."
+
+#: waeup/sirp/students/browser.py:837
+msgid "Course list request has been annulled."
+msgstr "Kursregistrierung wurde annulliert."
+
+#: waeup/sirp/students/browser.py:841
+msgid "Course list request has been rejected:"
+msgstr "Kursregistrierung wurde abgelehnt."
+
+#: waeup/sirp/students/browser.py:881
+#: waeup/sirp/students/browser.py:1811
+msgid "The ticket exists."
+msgstr "Das Ticket existiert."
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:883
+#: waeup/sirp/students/browser.py:1813
+msgid "Successfully added ${a}."
+msgstr "Erfolgreich hinzugefügt: ${a}"
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:904
+msgid "${a}: Course Ticket ${b}"
+msgstr "${a}: Kursticket ${b}"
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:920
+msgid "Manage course ticket ${a}"
+msgstr "Verwalte Kursticket ${a}"
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:947
+msgid "${a}: Payments"
+msgstr "${a}: Zahlungen"
+
+#: waeup/sirp/students/browser.py:992
+msgid "Add online payment"
+msgstr "Ticket für Online-Bezahlung hinzufügen"
+
+#: waeup/sirp/students/browser.py:995
+msgid "Create ticket"
+msgstr "Ticket erzeugen"
+
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:11
+msgid "Bed Category"
+msgstr "Betten-Kategorie"
+
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:29
+msgid "Registration Transition:"
+msgstr "Zustandsänderung der Registrierung:"
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/sirp/students/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/sirp/students/browser_templates/clearanceeditpage.pt:8
+msgid "Scans"
+msgstr "Scans"
+
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:28
+msgid "Select all"
+msgstr "Alle auswählen"
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:10
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:10
+msgid "with registration number"
+msgstr "mit Regsistrierungsnummer"
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:11
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:11
+msgid "with matriculation number"
+msgstr "mit Matrikelnummer"
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:12
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:12
+msgid "in session"
+msgstr "in der Session"
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:13
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:13
+msgid "in department"
+msgstr "im Department"
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:14
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:14
+msgid "in study mode"
+msgstr "im Studienmodus"
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:30
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:36
+msgid "Reg. Number"
+msgstr "Registrierungsnummer"
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:31
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:37
+msgid "Matric. Number"
+msgstr "Matrikelnummer"
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:32
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:38
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:14
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:25
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:87
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:54
+msgid "State"
+msgstr "Status"
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:33
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:39
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:44
+msgid "Full Name"
+msgstr "Vollständiger Name"
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:5
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:5
+msgid "for students"
+msgstr "nach Studenten"
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:8
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:8
+msgid "with id"
+msgstr "mit Id"
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:9
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:9
+msgid "with name"
+msgstr "mit Name"
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:31
+msgid "Semester:"
+msgstr "Semester:"
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:39
+msgid "Credits:"
+msgstr "Credits:"
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:47
+msgid "Passmark:"
+msgstr "Passmark:"
+
+#: waeup/sirp/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr "Aktivierungs-Code:"
+
+#: waeup/sirp/students/browser_templates/imageupload.pt:20
+#: waeup/sirp/students/browser_templates/fileupload.pt:27
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:25
+msgid "Max. file size:"
+msgstr "Max. Dateigröße:"
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:11
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:85
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:52
+msgid "Category"
+msgstr "Kategorie"
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:12
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:86
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:53
+msgid "Item"
+msgstr "Gegenstand"
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:8
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:82
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:49
+msgid "Payment Id"
+msgstr "Zahlungs-Id"
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:83
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:50
+msgid "Creation Date"
+msgstr "Erstellungsdatum"
+
+#: waeup/sirp/students/browser_templates/setpassword.pt:14
+msgid "Access Code:"
+msgstr "Zugangscode:"
+
+#: waeup/sirp/students/browser_templates/setpassword.pt:32
+msgid "Instructions:"
+msgstr "Anleitung:"
+
+#: waeup/sirp/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/sirp/students/browser_templates/setpassword.pt:5
+msgid "Registration Number:"
+msgstr "Regsistrierungsnummer:"
+
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:6
+msgid "Study Course Data"
+msgstr "Studiengangdaten"
+
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:9
+msgid "Study Levels"
+msgstr "Levels"
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr "Studienmodus:"
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:52
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:30
+msgid "Study Levels (Course Lists)"
+msgstr "Levels (Kurslisten)"
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:57
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:35
+msgid "Level Code"
+msgstr "Level-Kürzel"
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:58
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:36
+msgid "Level Title"
+msgstr "Level-Titel"
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:71
+msgid "There are no levels registered yet."
+msgstr "Es existieren bisher keine Levels registriert."
+
+#: waeup/sirp/students/browser_templates/studyleveladdpage.pt:4
+msgid "Text to explain course registration procedure."
+msgstr "Text zur Erklärung der Kursregistrierung."
+
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:15
+msgid "Department"
+msgstr "Department"
+
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:6
+msgid "Course Tickets (Total Credits: ${DYNAMIC_CONTENT})"
+msgstr "Kurstickets (Gesamte Credits: ${DYNAMIC_CONTENT})"
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:39
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:37
+msgid "Fact."
+msgstr "Fkt."
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:43
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:19
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:41
+msgid "CO"
+msgstr "CO"
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:44
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:42
+msgid "Auto"
+msgstr "Auto"
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:6
+msgid "Study Level Data"
+msgstr "Level-Daten"
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:9
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:30
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:28
+msgid "Course Tickets"
+msgstr "Kurstickets"
+
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:18
+msgid "Total Credits:"
+msgstr "Gesamte Credits"
+
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:64
+msgid "The're no tickets added yet."
+msgstr "Bisher wurden keine Tickets hinzugefügt."
+
+#: waeup/sirp/students/interfaces.py:176
+#: waeup/sirp/students/interfaces.py:255
+#: waeup/sirp/students/interfaces.py:483
+msgid "Registration Number"
+msgstr "Regsistrierungsnummer"
+
+#: waeup/sirp/students/interfaces.py:183
+#: waeup/sirp/students/interfaces.py:265
+msgid "Matriculation Number"
+msgstr "Matrikelnummer"
+
+#: waeup/sirp/students/interfaces.py:191
+msgid "PWD Activation Code"
+msgstr "PWD-Aktivierungs-Code"
+
+#: waeup/sirp/students/interfaces.py:198
+msgid "Email"
+msgstr "Emailadresse"
+
+#: waeup/sirp/students/interfaces.py:218
+msgid "Clearance form locked"
+msgstr "Einschreibungsformular gesperrt"
+
+#: waeup/sirp/students/interfaces.py:223
+msgid "CLR Activation Code"
+msgstr "CLR-Aktivierungs-Code"
+
+#: waeup/sirp/students/interfaces.py:231
+msgid "Nationality"
+msgstr "Nationalität"
+
+#: waeup/sirp/students/interfaces.py:241
+msgid "Permanent Address"
+msgstr "Postanschrift"
+
+#: waeup/sirp/students/interfaces.py:275
+#: waeup/sirp/browser/templates/showstudentspage.pt:7
+msgid "Certificate"
+msgstr "Zertifikat"
+
+#: waeup/sirp/students/interfaces.py:282
+msgid "Entry Mode"
+msgstr "Studienmodus bei Einschreibung"
+
+#: waeup/sirp/students/interfaces.py:290
+msgid "Entry Session"
+msgstr "Eingangssession"
+
+#: waeup/sirp/students/interfaces.py:299
+#: waeup/sirp/browser/templates/showstudentspage.pt:8
+msgid "Current Session"
+msgstr "Aktuelle Session"
+
+#: waeup/sirp/students/interfaces.py:307
+#: waeup/sirp/browser/templates/showstudentspage.pt:9
+msgid "Current Level"
+msgstr "Aktuelles Level"
+
+#: waeup/sirp/students/interfaces.py:315
+msgid "Current Verdict"
+msgstr "Aktuelles Verdict"
+
+#: waeup/sirp/students/interfaces.py:322
+msgid "Previous Verdict"
+msgstr "Vorangegangenes Verdict"
+
+#: waeup/sirp/students/interfaces.py:337
+#: waeup/sirp/students/interfaces.py:434
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:8
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:13
+msgid "Session"
+msgstr "Session"
+
+#: waeup/sirp/students/interfaces.py:344
+msgid "Verdict"
+msgstr "Verdict"
+
+#: waeup/sirp/students/interfaces.py:363
+#: waeup/sirp/browser/templates/certificatepage.pt:30
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:33
+msgid "Mandatory"
+msgstr "Pflicht"
+
+#: waeup/sirp/students/interfaces.py:377
+msgid "Automatical Creation"
+msgstr "Automatische Erzeugung"
+
+#: waeup/sirp/students/interfaces.py:384
+msgid "Carry-over Course"
+msgstr "Kurs aus vergangenem Studienjahr"
+
+#: waeup/sirp/students/interfaces.py:403
+#: waeup/sirp/university/interfaces.py:299
+#: waeup/sirp/browser/templates/certificatepage.pt:29
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:32
+msgid "Course"
+msgstr "Kurs"
+
+#: waeup/sirp/students/interfaces.py:420
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:10
+msgid "Bed Coordinates"
+msgstr "Bettkoordinaten"
+
+#: waeup/sirp/students/interfaces.py:442
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr "Buchungsdatum"
+
+#: waeup/sirp/students/interfaces.py:448
+msgid "Booking Activation Code"
+msgstr "Aktivierungs-Code der Buchung"
+
+#. Default: ""
+#: waeup/sirp/students/utils.py:163
+msgid "<font size=10>${a} Page ${b} of ${c}</font>"
+msgstr "<font size=10>${a}, Seite ${b} von ${c}</font>"
+
+#: waeup/sirp/students/utils.py:276
+#: waeup/sirp/students/viewlets.py:71
+#: waeup/sirp/students/viewlets.py:99
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:6
+msgid "Base Data"
+msgstr "Basisdaten"
+
+#: waeup/sirp/students/utils.py:307
+msgid "Scanned Documents"
+msgstr "Gescannte Dokumente"
+
+#: waeup/sirp/students/viewlets.py:109
+#: waeup/sirp/students/viewlets.py:533
+msgid "Personal Data"
+msgstr "Persönliche Daten"
+
+#: waeup/sirp/students/viewlets.py:131
+#: waeup/sirp/students/viewlets.py:537
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:27
+msgid "History"
+msgstr "Historie"
+
+#: waeup/sirp/students/viewlets.py:179
+#: waeup/sirp/students/workflow.py:72
+msgid "Clear student"
+msgstr "Student einschreiben"
+
+#: waeup/sirp/students/viewlets.py:194
+msgid "Reject clearance"
+msgstr "Einschreibung zurückweisen"
+
+#: waeup/sirp/students/viewlets.py:210
+msgid "Download clearance slip"
+msgstr "Einschreibungsbeleg herunterladen"
+
+#: waeup/sirp/students/viewlets.py:235
+msgid "Download course registration slip"
+msgstr "Kurseregistrierungsbeleg herunterladen"
+
+#: waeup/sirp/students/viewlets.py:251
+#: waeup/sirp/students/workflow.py:128
+msgid "Validate courses"
+msgstr "Kurse validieren"
+
+#: waeup/sirp/students/viewlets.py:267
+msgid "Reject courses"
+msgstr "Kurseregistrierung zurückweisen"
+
+#: waeup/sirp/students/viewlets.py:330
+msgid "Book accommodation"
+msgstr "Unterkunft buchen"
+
+#: waeup/sirp/students/viewlets.py:339
+msgid "Download bed allocation slip"
+msgstr "Unterkunftbeleg herunterladen"
+
+#: waeup/sirp/students/viewlets.py:348
+msgid "Relocate student"
+msgstr "Student umziehen"
+
+#: waeup/sirp/students/viewlets.py:374
+msgid "Change portrait"
+msgstr "Porträt ändern"
+
+#: waeup/sirp/students/viewlets.py:432
+msgid "Add course list"
+msgstr "Kursliste hinzufügen"
+
+#: waeup/sirp/students/viewlets.py:450
+msgid "Add and remove courses"
+msgstr "Kurs hinzufügen oder löschen"
+
+#: waeup/sirp/students/viewlets.py:536
+msgid "Accommodation Data"
+msgstr "Unterkunft"
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:548
+msgid "${a} deleted."
+msgstr "${a} gelöscht."
+
+#: waeup/sirp/students/viewlets.py:561
+msgid "File name has no extension."
+msgstr "Dateiname hat keine Endung."
+
+#: waeup/sirp/students/viewlets.py:564
+msgid "File name contains more than one dot."
+msgstr "Dateiname enthält mehr als einen Punkt."
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:571
+msgid "${a} file extension expected."
+msgstr "${a} Dateiendung erwartet."
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:577
+msgid "Only the following extension are allowed: ${a}"
+msgstr "Nur die folgenden Erweiterungen sind erlaubt: ${a}."
+
+#: waeup/sirp/students/viewlets.py:583
+msgid "Uploaded file is too big."
+msgstr "Das hochgeladene Bild ist zu groß!"
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:590
+msgid "File ${a} uploaded."
+msgstr "Datei ${a} hochgeladen."
+
+#: waeup/sirp/students/viewlets.py:608
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:10
+#: waeup/sirp/browser/templates/datacenterpage.pt:14
+#: waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt:11
+msgid "File"
+msgstr "Datei"
+
+#: waeup/sirp/students/viewlets.py:609
+msgid "Scan"
+msgstr "Scan"
+
+#: waeup/sirp/students/viewlets.py:632
+msgid "Upload new file"
+msgstr "Neue Datei hochladen"
+
+#: waeup/sirp/students/viewlets.py:633
+msgid "Delete attachment"
+msgstr "Anhang löschen"
+
+#: waeup/sirp/students/viewlets.py:663
+msgid "No local file selected."
+msgstr "Keine lokale Datei ausgewählt."
+
+#: waeup/sirp/students/viewlets.py:677
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:8
+msgid "Passport Picture"
+msgstr "Passbild"
+
+#: waeup/sirp/students/viewlets.py:688
+msgid "Passport Picture (jpg only)"
+msgstr "Passbild (nur jpg)"
+
+#: waeup/sirp/students/viewlets.py:703
+#: waeup/sirp/students/viewlets.py:714
+msgid "Birth Certificate"
+msgstr "Geburtsurkunde"
+
+#: waeup/sirp/students/viewlets.py:704
+#: waeup/sirp/students/viewlets.py:715
+msgid "Birth Certificate Scan"
+msgstr "Geburtsurkunde-Scan"
+
+#: waeup/sirp/students/viewlets.py:724
+#: waeup/sirp/students/viewlets.py:735
+msgid "Acceptance Letter"
+msgstr "Acceptance Letter"
+
+#: waeup/sirp/students/viewlets.py:725
+#: waeup/sirp/students/viewlets.py:736
+msgid "Acceptance Letter Scan"
+msgstr "Acceptance Letter Scan"
+
+#: waeup/sirp/students/viewlets.py:85
+msgid "Application Slip"
+msgstr "Antrag"
+
+#: waeup/sirp/students/vocabularies.py:135
+msgid "Male"
+msgstr "männlich"
+
+#: waeup/sirp/students/vocabularies.py:137
+msgid "Female"
+msgstr "weiblich"
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:77
+msgid "Error: level id ${value} out of range"
+msgstr "Fehler: level_id ${value} nicht verfügbar"
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:88
+msgid "${title} 2nd spillover"
+msgstr "${title} / 2. Nachholen"
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:91
+msgid "${title} 3rd spillover"
+msgstr "${title} / 3. Nachholen"
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:94
+msgid "${title} 1st spillover"
+msgstr "${title} / 1. Nachholen"
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:96
+msgid "${title} on 1st probation"
+msgstr "${title} / 1. Wiederholung"
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:98
+msgid "${title} on 2nd probation"
+msgstr "${title} / 2. Wiederholung"
+
+#: waeup/sirp/students/workflow.py:107
+msgid "Reset to returning"
+msgstr "Setze zurück auf 'rückgemeldet'"
+
+#: waeup/sirp/students/workflow.py:108
+msgid "Student record reset to 'returning'"
+msgstr "Student zurückgesetzt auf 'rückgemeldet'"
+
+#: waeup/sirp/students/workflow.py:114
+msgid "Register courses"
+msgstr "Kurse registrieren"
+
+#: waeup/sirp/students/workflow.py:115
+msgid "Courses registered"
+msgstr "Kurse registriert"
+
+#: waeup/sirp/students/workflow.py:121
+#: waeup/sirp/students/workflow.py:135
+msgid "Reset to paid"
+msgstr "Auf 'Gebühren bezahlt' zurücksetzen"
+
+#: waeup/sirp/students/workflow.py:122
+#: waeup/sirp/students/workflow.py:136
+msgid "Student record reset to 'paid'"
+msgstr "Student zurückgesetzt auf 'Gebühren bezahlt'"
+
+#: waeup/sirp/students/workflow.py:129
+msgid "Courses validated"
+msgstr "Kurse validiert"
+
+#: waeup/sirp/students/workflow.py:142
+msgid "Return"
+msgstr "Zurückmelden"
+
+#: waeup/sirp/students/workflow.py:143
+msgid "Returned"
+msgstr "zurückgemeldet"
+
+#: waeup/sirp/students/workflow.py:149
+msgid "Reset to validated"
+msgstr "Auf 'Kurse validiert' zurücksetzen"
+
+#: waeup/sirp/students/workflow.py:150
+msgid "Student record reset to 'validated'"
+msgstr "Student zurückgesetzt auf 'Kurse validiert'"
+
+#: waeup/sirp/students/workflow.py:22
+msgid "Create student"
+msgstr "Studentendatensatz erzeugen"
+
+#: waeup/sirp/students/workflow.py:30
+msgid "Admit student"
+msgstr "Student zulassen"
+
+#: waeup/sirp/students/workflow.py:31
+msgid "Student admitted"
+msgstr "Student zugelassen"
+
+#: waeup/sirp/students/workflow.py:37
+msgid "Reset student"
+msgstr "Zurücksetzen"
+
+#: waeup/sirp/students/workflow.py:38
+msgid "Student record reset"
+msgstr "Student zurückgesetzt"
+
+#: waeup/sirp/students/workflow.py:45
+msgid "Clearance started"
+msgstr "Einschreibung gestartet"
+
+#: waeup/sirp/students/workflow.py:51
+msgid "Reset to admitted"
+msgstr "Auf 'zugelassen' zurücksetzen"
+
+#: waeup/sirp/students/workflow.py:52
+msgid "Student record reset to 'admitted'"
+msgstr "Student zurückgesetzt auf 'zugelassen'"
+
+#: waeup/sirp/students/workflow.py:59
+msgid "Clearance requested"
+msgstr "Einschreibung beantragt"
+
+#: waeup/sirp/students/workflow.py:65
+#: waeup/sirp/students/workflow.py:79
+msgid "Reset to clearance"
+msgstr "Auf 'eingeschrieben' zurücksetzen"
+
+#: waeup/sirp/students/workflow.py:66
+#: waeup/sirp/students/workflow.py:80
+msgid "Student record reset to 'clearance'"
+msgstr "Student zurückgesetzt auf 'Einschreibung begonnen'"
+
+#: waeup/sirp/students/workflow.py:73
+msgid "Cleared"
+msgstr "eingeschrieben"
+
+#: waeup/sirp/students/workflow.py:86
+#: waeup/sirp/students/workflow.py:100
+msgid "Pay school fee"
+msgstr "Gebühren bezahlen"
+
+#: waeup/sirp/students/workflow.py:87
+#: waeup/sirp/students/workflow.py:101
+msgid "School fee paid"
+msgstr "Gebühren bezahlt"
+
+#: waeup/sirp/students/workflow.py:93
+msgid "Reset to cleared"
+msgstr "Auf 'eingeschrieben' zurücksetzen"
+
+#: waeup/sirp/students/workflow.py:94
+msgid "Student record reset to 'cleared'"
+msgstr "Student zurückgesetzt auf 'eingeschrieben'"
+
+#: waeup/sirp/university/interfaces.py:146
+msgid "Title of course"
+msgstr "Titel des Kurses"
+
+#: waeup/sirp/university/interfaces.py:152
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:17
+msgid "Credits"
+msgstr "Credits"
+
+#: waeup/sirp/university/interfaces.py:158
+msgid "Passmark"
+msgstr "Passmark"
+
+#: waeup/sirp/university/interfaces.py:164
+msgid "Semester/Term"
+msgstr "Semester/Term"
+
+#: waeup/sirp/university/interfaces.py:180
+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/sirp/university/interfaces.py:205
+msgid "Study Mode"
+msgstr "Studienmodus"
+
+#: waeup/sirp/university/interfaces.py:212
+msgid "Start Level"
+msgstr "Einstiegslevel"
+
+#: waeup/sirp/university/interfaces.py:219
+msgid "End Level"
+msgstr "Endlevel"
+
+#: waeup/sirp/university/interfaces.py:226
+msgid "Application Category"
+msgstr "Antragskategorie"
+
+#: waeup/sirp/university/interfaces.py:243
+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/sirp/university/interfaces.py:264
+msgid "Course Referrer"
+msgstr "Kursreferenz"
+
+#: waeup/sirp/university/interfaces.py:270
+#: waeup/sirp/browser/templates/certificatepage.pt:28
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:30
+msgid "Level"
+msgstr "Level"
+
+#: waeup/sirp/university/interfaces.py:277
+msgid "Is mandatory course (not elective)"
+msgstr "Pflichtkurs"
+
+#: waeup/sirp/university/interfaces.py:45
+msgid "Name of faculty"
+msgstr "Name der Fakultät"
+
+#: waeup/sirp/university/interfaces.py:46
+#: waeup/sirp/university/interfaces.py:94
+#: waeup/sirp/university/interfaces.py:147
+#: waeup/sirp/university/interfaces.py:200
+msgid "Unnamed"
+msgstr "Unbenannt"
+
+#: waeup/sirp/university/interfaces.py:51
+#: waeup/sirp/university/interfaces.py:99
+msgid "Name prefix"
+msgstr "Prefix"
+
+#: waeup/sirp/university/interfaces.py:67
+#: waeup/sirp/university/interfaces.py:118
+msgid "This code will become part of the URL."
+msgstr "Dieses Kürzel wird Teil der URL sein."
+
+#: waeup/sirp/university/interfaces.py:93
+msgid "Name of department"
+msgstr "Name des Departments"
+
+#: waeup/sirp/university/vocabularies.py:28
+msgid "Pre-Studies"
+msgstr "Vorstudium"
+
+#: waeup/sirp/university/vocabularies.py:29
+msgid "100 (Year 1)"
+msgstr "1. Studienjahr"
+
+#: waeup/sirp/university/vocabularies.py:30
+msgid "200 (Year 2)"
+msgstr "2. Studienjahr"
+
+#: waeup/sirp/university/vocabularies.py:31
+msgid "300 (Year 3)"
+msgstr "3. Studienjahr"
+
+#: waeup/sirp/university/vocabularies.py:32
+msgid "400 (Year 4)"
+msgstr "4. Studienjahr"
+
+#: waeup/sirp/university/vocabularies.py:33
+msgid "500 (Year 5)"
+msgstr "5. Studienjahr"
+
+#: waeup/sirp/university/vocabularies.py:34
+msgid "600 (Year 6)"
+msgstr "6. Studienjahr"
+
+#: waeup/sirp/university/vocabularies.py:35
+msgid "700 (Year 7)"
+msgstr "7. Studienjahr"
+
+#: waeup/sirp/university/vocabularies.py:36
+msgid "800 (Year 8)"
+msgstr "8. Studienjahr"
+
+#: waeup/sirp/utils/utils.py:119
+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/sirp/utils/utils.py:164
+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"
+"Login page: ${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"
+"Login-Seite: ${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"
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/en/LC_MESSAGES/waeup.sirp.po
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/en/LC_MESSAGES/waeup.sirp.po	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/en/LC_MESSAGES/waeup.sirp.po	(revision 7760)
@@ -0,0 +1,3570 @@
+# #############################################################################
+#
+# 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.SIRP\n"
+"POT-Creation-Date: Fri Mar  2 12:58:47 2012\n"
+"PO-Revision-Date: 2012-02-29 12:32+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/sirp/accesscodes/browser.py:100 waeup/sirp/accesscodes/browser.py:113
+#: waeup/sirp/applicants/browser.py:80 waeup/sirp/applicants/browser.py:130
+#: waeup/sirp/applicants/browser.py:182 waeup/sirp/applicants/browser.py:254
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:338
+#: waeup/sirp/browser/pages.py:503 waeup/sirp/browser/pages.py:708
+#: waeup/sirp/browser/pages.py:735 waeup/sirp/browser/pages.py:758
+#: waeup/sirp/browser/pages.py:805 waeup/sirp/browser/pages.py:835
+#: waeup/sirp/browser/pages.py:955 waeup/sirp/browser/pages.py:1183
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1298
+#: waeup/sirp/browser/pages.py:1325 waeup/sirp/browser/pages.py:1353
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1400
+#: waeup/sirp/browser/pages.py:1441 waeup/sirp/browser/pages.py:1520
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1523
+#: waeup/sirp/browser/pages.py:1601 waeup/sirp/browser/pages.py:1646
+#: waeup/sirp/browser/pages.py:1684 waeup/sirp/browser/pages.py:1728
+#: waeup/sirp/browser/pages.py:1767 waeup/sirp/browser/pages.py:1769
+#: waeup/sirp/browser/pages.py:1804 waeup/sirp/browser/pages.py:1855
+#: waeup/sirp/browser/pages.py:1907 waeup/sirp/students/browser.py:584
+#: waeup/sirp/students/browser.py:585 waeup/sirp/students/browser.py:746
+#: waeup/sirp/students/browser.py:748 waeup/sirp/students/browser.py:888
+msgid "Cancel"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:102
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:112
+msgid "Reimport"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:115
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:120
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:126
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:136
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:139
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:152
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:153 waeup/sirp/browser/pages.py:592
+#: waeup/sirp/browser/pages.py:1142 waeup/sirp/browser/viewlets.py:190
+#: waeup/sirp/students/browser.py:171 waeup/sirp/students/browser.py:205
+msgid "Search"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:154
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:155
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:180 waeup/sirp/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:182
+msgid "${a} disabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:186
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:190
+msgid "re-enabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:192
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:196
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:235
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:244
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:252
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:261
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:43
+msgid "Archive"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:44
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:46 waeup/sirp/accesscodes/browser.py:206
+#: waeup/sirp/accesscodes/browser.py:226
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:53
+msgid "No batch selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:61
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:68
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:77
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:83
+msgid "Create batch"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:91
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:93
+msgid "Data written to ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:10
+msgid "Entries/(invalidated)"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:11
+msgid "Cost"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:12
+msgid "Date of Creation"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:13
+msgid "Creator"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:2
+msgid "The following batches are available:"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:39
+msgid "No batches yet"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:9
+msgid "Prefix"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt:23
+msgid "No import batches available"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:23
+msgid "Serial"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:24
+msgid "AC"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:26
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:38
+msgid "Owner"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:5
+msgid "for access codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:7
+msgid "with PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:8
+msgid "with serial"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:9
+msgid "with message term"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:29
+msgid "Serial number inside batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:32
+msgid "Prefix inside batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:35
+msgid "Batch number"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:38
+msgid "Random part of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:41
+#: waeup/sirp/accesscodes/interfaces.py:80
+msgid "Cost of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:45
+msgid "Workflow state"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:48
+msgid "Complete title of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:51
+msgid "Purchaser"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:54
+msgid "The history of access code as lines"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:63
+msgid "Creation date"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:66
+msgid "Batch creator"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:69
+msgid "Batch prefix"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:72
+msgid "Batch number (1-3 digits)"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:76
+msgid "Number of access codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:84
+msgid "Number of disabled access codes inside the batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:89
+msgid "Number of used access codes inside the batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:102
+msgid "Disable used PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:109
+msgid "Reenable disabled PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:37 waeup/sirp/applicants/workflow.py:37
+msgid "initialized"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:38
+msgid "used"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:81
+msgid "Initialize PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:88
+msgid "Use PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:95
+msgid "Disable unused PIN"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:117 waeup/sirp/applicants/browser.py:325
+#: waeup/sirp/applicants/browser.py:745
+msgid "Could not delete:"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:120 waeup/sirp/applicants/browser.py:328
+#: waeup/sirp/applicants/browser.py:748 waeup/sirp/browser/pages.py:134
+#: waeup/sirp/students/browser.py:243 waeup/sirp/students/browser.py:642
+#: waeup/sirp/students/browser.py:796 waeup/sirp/students/browser.py:974
+#: waeup/sirp/students/browser.py:1184 waeup/sirp/students/browser.py:1770
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:169
+msgid ""
+"An applicants container for the same application type and entrance year "
+"exists already in the database."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:178
+msgid "Added:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:190 waeup/sirp/applicants/viewlets.py:44
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:12
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:36
+msgid "Applicants"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:254 waeup/sirp/applicants/browser.py:301
+#: waeup/sirp/applicants/browser.py:654 waeup/sirp/applicants/browser.py:688
+#: waeup/sirp/applicants/browser.py:791 waeup/sirp/applicants/browser.py:794
+#: waeup/sirp/applicants/browser.py:838 waeup/sirp/browser/pages.py:476
+#: waeup/sirp/browser/pages.py:631 waeup/sirp/browser/pages.py:654
+#: waeup/sirp/browser/pages.py:728 waeup/sirp/browser/pages.py:1181
+#: waeup/sirp/browser/pages.py:1353 waeup/sirp/browser/pages.py:1394
+#: waeup/sirp/browser/pages.py:1520 waeup/sirp/browser/pages.py:1573
+#: waeup/sirp/browser/pages.py:1716 waeup/sirp/browser/pages.py:1767
+#: waeup/sirp/browser/pages.py:1788 waeup/sirp/hostels/browser.py:209
+#: waeup/sirp/hostels/browser.py:231 waeup/sirp/hostels/browser.py:319
+#: waeup/sirp/students/browser.py:359 waeup/sirp/students/browser.py:458
+#: waeup/sirp/students/browser.py:535 waeup/sirp/students/browser.py:584
+#: waeup/sirp/students/browser.py:600 waeup/sirp/students/browser.py:746
+#: waeup/sirp/students/browser.py:771 waeup/sirp/students/browser.py:1444
+#: waeup/sirp/students/browser.py:1459 waeup/sirp/students/browser.py:1574
+msgid "Save"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:333
+#: waeup/sirp/applicants/browser.py:361
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:265 waeup/sirp/applicants/viewlets.py:95
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:305 waeup/sirp/applicants/browser.py:717
+#: waeup/sirp/browser/pages.py:1397 waeup/sirp/browser/pages.py:1719
+#: waeup/sirp/browser/pages.py:1791 waeup/sirp/browser/pages.py:1903
+#: waeup/sirp/hostels/browser.py:54 waeup/sirp/students/browser.py:80
+#: waeup/sirp/students/browser.py:385
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:314
+msgid "No applicant selected!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:364
+msgid "Create application record"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:369
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:382
+msgid "Applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:397 waeup/sirp/students/browser.py:285
+msgid "set"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:398 waeup/sirp/students/browser.py:286
+msgid "unset"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:403
+msgid "${a} Application Record ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:454
+msgid "Session configuration object is not available."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:461 waeup/sirp/students/browser.py:1023
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:474 waeup/sirp/students/browser.py:1037
+msgid "Payment ticket created."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:501 waeup/sirp/students/browser.py:1056
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:529 waeup/sirp/students/browser.py:1105
+msgid "Valid callback received."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:550
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:556
+msgid "Online Payment Receipt"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:562
+msgid "Ticket not yet paid."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:627
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:632
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:64
+msgid "Application Section"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:654 waeup/sirp/applicants/browser.py:794
+#: waeup/sirp/applicants/browser.py:846
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:655 waeup/sirp/applicants/browser.py:758
+#: waeup/sirp/applicants/browser.py:792 waeup/sirp/applicants/browser.py:827
+#: waeup/sirp/students/browser.py:980
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:655 waeup/sirp/applicants/browser.py:763
+#: waeup/sirp/applicants/browser.py:792 waeup/sirp/applicants/browser.py:795
+#: waeup/sirp/applicants/browser.py:832 waeup/sirp/students/browser.py:748
+#: waeup/sirp/students/browser.py:780 waeup/sirp/students/browser.py:956
+#: waeup/sirp/students/browser.py:1752
+msgid "Remove selected tickets"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:674
+msgid "${a} Application Form ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:685 waeup/sirp/students/browser.py:356
+msgid "No transition"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:732 waeup/sirp/students/browser.py:962
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:76 waeup/sirp/applicants/viewlets.py:88
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:79 waeup/sirp/applicants/browser.py:105
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:308
+#: waeup/sirp/browser/pages.py:634 waeup/sirp/browser/pages.py:675
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1288
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1389
+#: waeup/sirp/hostels/browser.py:131 waeup/sirp/students/browser.py:206
+#: waeup/sirp/students/browser.py:1151 waeup/sirp/students/browser.py:1164
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:79 waeup/sirp/applicants/browser.py:125
+#: waeup/sirp/applicants/browser.py:148 waeup/sirp/applicants/browser.py:160
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:805 waeup/sirp/students/browser.py:88
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:81 waeup/sirp/applicants/browser.py:139
+#: waeup/sirp/applicants/browser.py:256 waeup/sirp/applicants/browser.py:347
+#: waeup/sirp/browser/pages.py:1355 waeup/sirp/browser/pages.py:1414
+#: waeup/sirp/browser/pages.py:1524 waeup/sirp/browser/pages.py:1610
+#: waeup/sirp/browser/pages.py:1770 waeup/sirp/browser/pages.py:1827
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:819
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:82 waeup/sirp/applicants/browser.py:135
+#: waeup/sirp/applicants/browser.py:257 waeup/sirp/applicants/browser.py:343
+#: waeup/sirp/browser/pages.py:1356 waeup/sirp/browser/pages.py:1410
+#: waeup/sirp/browser/pages.py:1525 waeup/sirp/browser/pages.py:1606
+#: waeup/sirp/browser/pages.py:1771 waeup/sirp/browser/pages.py:1823
+msgid "Add local role"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:821
+msgid "Passport picture confirmation box not ticked."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:83
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:858 waeup/sirp/students/browser.py:1619
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:863
+msgid "Form has been submitted."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:898
+msgid "Register for ${a} Application"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:904
+msgid "Application has not yet started."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:909
+msgid "Application has ended."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:918
+msgid "Get login credentials"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:933
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:940
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:949
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:18
+msgid "Photograph"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:43
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:59
+msgid ""
+"I confirm that the Passport Photograph uploaded on this form is a true "
+"picture of me."
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:74
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:43
+msgid "Acceptance Fee Payment Tickets"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:11
+msgid "Thanks for using SIRP!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:3
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/sirp/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/sirp/applicants/browser_templates/applicantregister.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:42
+msgid "Application Number"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:46
+msgid "First Choice"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:48
+msgid "Application State"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:4
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:23
+msgid "Year"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:7
+msgid "Application Period"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:197 waeup/sirp/students/browser.py:713
+#: waeup/sirp/university/interfaces.py:38
+#: waeup/sirp/university/interfaces.py:66
+#: waeup/sirp/university/interfaces.py:86
+#: waeup/sirp/university/interfaces.py:117
+#: waeup/sirp/university/interfaces.py:139
+#: waeup/sirp/university/interfaces.py:179
+#: waeup/sirp/university/interfaces.py:192
+#: waeup/sirp/university/interfaces.py:241
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:39
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:68
+#: waeup/sirp/browser/templates/facultypage.pt:8
+#: waeup/sirp/browser/templates/departmentpage.pt:15
+#: waeup/sirp/browser/templates/departmentpage.pt:39
+#: waeup/sirp/browser/templates/searchpage.pt:13
+#: waeup/sirp/browser/templates/facultymanagepage.pt:41
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:36
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:13
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:34
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:5
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:25
+msgid "Code"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:204 waeup/sirp/students/browser.py:714
+#: waeup/sirp/university/interfaces.py:199
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:41
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:70
+#: waeup/sirp/browser/templates/facultypage.pt:9
+#: waeup/sirp/browser/templates/departmentpage.pt:16
+#: waeup/sirp/browser/templates/departmentpage.pt:40
+#: waeup/sirp/browser/templates/searchpage.pt:14
+#: waeup/sirp/browser/templates/certificatepage.pt:30
+#: waeup/sirp/browser/templates/facultymanagepage.pt:43
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:32
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:37
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:35
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:6
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:27
+msgid "Title"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:211
+#: waeup/sirp/applicants/interfaces.py:324
+msgid "Application Target"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:219
+msgid "Entry Level"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:226
+#: waeup/sirp/applicants/interfaces.py:332
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:234
+#: waeup/sirp/applicants/interfaces.py:340
+msgid "Applicants Container Type"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:250
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:257
+msgid "Human readable description in reST format"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:273
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:279
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:285
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:368
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:373
+msgid "JAMB Registration Number"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:385
+#: waeup/sirp/students/interfaces.py:154
+msgid "First Name"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:389
+#: waeup/sirp/students/interfaces.py:159
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:393
+#: waeup/sirp/students/interfaces.py:164
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:397
+#: waeup/sirp/students/interfaces.py:213
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:402
+msgid "State/LGA"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:407
+#: waeup/sirp/students/interfaces.py:169
+msgid "Sex"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:413
+#: waeup/sirp/students/interfaces.py:490
+msgid "Email Address"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:418
+#: waeup/sirp/students/interfaces.py:203
+msgid "Phone"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:423
+#: waeup/sirp/applicants/interfaces.py:503
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:428
+#: waeup/sirp/applicants/interfaces.py:508
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:437
+#: waeup/sirp/applicants/interfaces.py:513
+msgid "Screening Score"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:441
+#: waeup/sirp/applicants/interfaces.py:518
+msgid "Screening Venue"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:445
+#: waeup/sirp/applicants/interfaces.py:523
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:451
+#: waeup/sirp/applicants/interfaces.py:530
+msgid "Notice"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:473
+#: waeup/sirp/students/interfaces.py:149
+#: waeup/sirp/browser/templates/showstudentspage.pt:4
+#: waeup/sirp/students/browser_templates/containerpage.pt:29
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:35
+msgid "Student Id"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:478
+msgid "Form locked"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:557
+#: waeup/sirp/students/interfaces.py:469
+msgid "Payment Session"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:110
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:23
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:121
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:15
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:182
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:26
+msgid "Admitted Course of Study:"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/pdf.py:200
+msgid ""
+"<font size=10>Proceed to the login page of the portal and enter your new "
+"credentials: user name= ${a}, password = ${b}.</font>"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:206
+msgid "<font size=10>Change your password when you have  logged in.</font>"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:57
+msgid "Application Record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:103
+msgid "Register for application"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:111
+msgid "View application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:119
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:127
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:143
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:151 waeup/sirp/applicants/workflow.py:99
+#: waeup/sirp/students/browser.py:258
+msgid "Create student record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:168 waeup/sirp/students/viewlets.py:300
+msgid "Download payment receipt"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:183 waeup/sirp/students/viewlets.py:315
+msgid "Request callback"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:57
+msgid "Application"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:73 waeup/sirp/students/viewlets.py:512
+msgid "My Data"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:100 waeup/sirp/students/workflow.py:25
+msgid "Student record created"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:106 waeup/sirp/applicants/workflow.py:113
+#: waeup/sirp/applicants/workflow.py:120 waeup/sirp/applicants/workflow.py:127
+msgid "Reset application to started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:107 waeup/sirp/applicants/workflow.py:114
+#: waeup/sirp/applicants/workflow.py:121 waeup/sirp/applicants/workflow.py:128
+#: waeup/sirp/applicants/workflow.py:134
+msgid "Application reset"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:133
+msgid "Reset application to paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:38
+msgid "started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:39
+msgid "paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:40
+msgid "submitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:41 waeup/sirp/interfaces.py:71
+msgid "admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:42
+msgid "not admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:43 waeup/sirp/interfaces.py:70
+msgid "created"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:49
+msgid "Initialize application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:52
+msgid "Application initialized"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:57
+msgid "Start application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:58
+msgid "Application started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:64
+msgid "Pay acceptance fee"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:65
+msgid "Fee paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:71
+msgid "Submit application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:72
+msgid "Application submitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:78
+msgid "Admit applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:79
+msgid "Applicant admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:85 waeup/sirp/applicants/workflow.py:92
+msgid "Refuse application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:86 waeup/sirp/applicants/workflow.py:93
+msgid "Application refused"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:104
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:116 waeup/sirp/browser/viewlets.py:615
+msgid "Academics"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:123 waeup/sirp/browser/pages.py:309
+msgid "Administration"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:130 waeup/sirp/browser/viewlets.py:360
+#: waeup/sirp/browser/viewlets.py:666
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:148 waeup/sirp/browser/pages.py:386
+#: waeup/sirp/browser/viewlets.py:375 waeup/sirp/browser/viewlets.py:676
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:155 waeup/sirp/browser/pages.py:748
+#: waeup/sirp/browser/pages.py:1140 waeup/sirp/browser/viewlets.py:393
+#: waeup/sirp/browser/viewlets.py:685
+msgid "Data Center"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:90
+msgid "Home"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1016
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:103
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1063
+msgid "View processing log"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1064
+msgid "Back to data center"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1108
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1110
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1118
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1120
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1121
+msgid "Show"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:113 waeup/sirp/hostels/browser.py:256
+#: waeup/sirp/hostels/browser.py:287 waeup/sirp/students/browser.py:230
+msgid "No item selected."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1143
+msgid "Back"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1151
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1179
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1199
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1202
+msgid "Given storage path cannot be used."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1203
+msgid "Error: ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1206
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1265
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1293
+#: waeup/sirp/browser/pages.py:1310 waeup/sirp/browser/pages.py:1314
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1278
+msgid "Faculties"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1282 waeup/sirp/browser/viewlets.py:441
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1321
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1342 waeup/sirp/browser/pages.py:1352
+msgid "Departments"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1405
+#: waeup/sirp/browser/pages.py:1424 waeup/sirp/browser/pages.py:1428
+msgid "Add department"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1362 waeup/sirp/browser/viewlets.py:457
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1435
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1438
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1455
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1485 waeup/sirp/browser/pages.py:1504
+#: waeup/sirp/students/browser.py:96 waeup/sirp/students/viewlets.py:472
+msgid "Students"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:152 waeup/sirp/browser/viewlets.py:322
+#: waeup/sirp/browser/templates/staffsitelayout.pt:28
+#: waeup/sirp/browser/templates/studentsitelayout.pt:33
+msgid "Login"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1579
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1591
+#: waeup/sirp/browser/pages.py:1620 waeup/sirp/browser/pages.py:1624
+msgid "Add course"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1522 waeup/sirp/browser/pages.py:1585
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1522 waeup/sirp/browser/pages.py:1596
+#: waeup/sirp/browser/pages.py:1657 waeup/sirp/browser/pages.py:1661
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1531 waeup/sirp/browser/viewlets.py:464
+msgid "Manage department"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:160
+msgid "You logged in."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1636
+msgid "A course with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1642
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1673
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1679
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1711 waeup/sirp/browser/viewlets.py:481
+msgid "Edit course"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1761
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1768 waeup/sirp/browser/pages.py:1799
+#: waeup/sirp/browser/pages.py:1840 waeup/sirp/browser/pages.py:1842
+msgid "Add course referrer"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1769 waeup/sirp/browser/pages.py:1794
+msgid "Remove selected course referrers"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1775 waeup/sirp/browser/viewlets.py:488
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:178
+msgid "You entered wrong credentials."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1847
+msgid "The chosen course referrer is already part of this certificate."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1851
+msgid "Course referrer ${a}_${b} added."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1888 waeup/sirp/browser/viewlets.py:505
+msgid "Edit course referrer"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1891
+msgid "Save and return"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:192
+msgid "You have been logged out. Thanks for using WAeUP SIRP!"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:228
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:234 waeup/sirp/browser/pages.py:519
+msgid "Send message now"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:253 waeup/sirp/browser/pages.py:272
+#: waeup/sirp/browser/pages.py:536 waeup/sirp/students/browser.py:320
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:263
+msgid "Send now"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:274
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:299
+msgid "<h1>Welcome to WAeUP.SIRP</h1>"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:387 waeup/sirp/hostels/browser.py:195
+#: waeup/sirp/students/viewlets.py:163 waeup/sirp/students/viewlets.py:171
+#: waeup/sirp/students/viewlets.py:226 waeup/sirp/students/viewlets.py:243
+#: waeup/sirp/students/viewlets.py:283
+msgid "Manage"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:388 waeup/sirp/browser/viewlets.py:180
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:395
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:426 waeup/sirp/browser/pages.py:428
+#: waeup/sirp/browser/viewlets.py:515
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:450
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:469
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:500
+msgid "User settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:517 waeup/sirp/students/browser.py:301
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:546 waeup/sirp/browser/viewlets.py:400
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:559 waeup/sirp/browser/viewlets.py:415
+msgid "My Roles"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:590
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:617
+msgid "View portal configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:630
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:631 waeup/sirp/browser/pages.py:680
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:633 waeup/sirp/browser/pages.py:661
+#: waeup/sirp/browser/pages.py:692
+msgid "Add session configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:658 waeup/sirp/browser/pages.py:731
+msgid "Settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:672
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:683
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:696
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:703
+msgid "The session chosen already exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:725
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:755
+msgid "Upload file"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:757
+msgid "Upload"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:803 waeup/sirp/browser/pages.py:833
+#: waeup/sirp/browser/pages.py:953 waeup/sirp/browser/pages.py:1061
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:817 waeup/sirp/browser/pages.py:918
+#: waeup/sirp/browser/pages.py:1037
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:83
+msgid "No local role selected."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:836
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:837
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:897
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:905
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:930
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:956 waeup/sirp/browser/pages.py:1182
+msgid "Reset"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:957
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:958
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:959
+msgid "Perform import"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:10
+msgid "Course Code:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:14
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:7
+msgid "Course Title:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:18
+msgid "Provided by:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:26
+msgid "Level:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:30
+msgid "Is mandatory course (not elective):"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:6
+msgid "Code:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:22
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:6
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:26
+msgid "Course Referrers"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:28
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:31
+msgid "Semester"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:29
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:31
+msgid "Referrer"
+msgstr ""
+
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr ""
+
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:8
+msgid "Session Configurations"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:14
+msgid "Datasets"
+msgstr ""
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:43
+msgid "Mode:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:12
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:6
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:4
+msgid "File:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:18
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:9
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:32
+msgid "Processor:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:24
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:13
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:44
+msgid "change to:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:12
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:16
+msgid "Date"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:14
+#: waeup/sirp/browser/templates/datacenterpage.pt:15
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:12
+msgid "Overwrite contents in new location?"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:3
+#: waeup/sirp/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:7
+msgid "Copy existing data to new storage?"
+msgstr ""
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterpage.pt:16
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:10
+#: waeup/sirp/browser/templates/departmentpage.pt:3
+msgid "Courses"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:101
+#: waeup/sirp/browser/templates/facultymanagepage.pt:75
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:92
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:93
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:60
+msgid "Local Role"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:13
+#: waeup/sirp/browser/templates/departmentpage.pt:6
+msgid "Certificates"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:16
+#: waeup/sirp/browser/templates/userscontainerpage.pt:7
+#: waeup/sirp/browser/templates/facultymanagepage.pt:16
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:7
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:17
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:11
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:6
+#: waeup/sirp/browser/templates/facultymanagepage.pt:6
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:5
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:7
+msgid "Settings"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:97
+#: waeup/sirp/browser/templates/userscontainerpage.pt:4
+#: waeup/sirp/browser/templates/facultymanagepage.pt:71
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:88
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:89
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:56
+msgid "User Id"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:99
+#: waeup/sirp/browser/templates/userscontainerpage.pt:5
+#: waeup/sirp/browser/templates/facultymanagepage.pt:73
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:31
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:90
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:91
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:58
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:10
+#: waeup/sirp/hostels/browser_templates/containermanagepage.pt:11
+msgid "Name"
+msgstr ""
+
+#: waeup/sirp/browser/templates/facultypage.pt:1
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:1
+msgid "There no subobjects registered yet."
+msgstr ""
+
+#: waeup/sirp/browser/templates/loginpage.pt:13
+#: waeup/sirp/browser/templates/usereditformpage.pt:8
+#: waeup/sirp/students/browser_templates/basepage.pt:14
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:17
+#: waeup/sirp/students/browser_templates/change_password.pt:10
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:31
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:34
+msgid "Password:"
+msgstr ""
+
+#: waeup/sirp/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/sirp/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 Access Code (PWD) and inititialize your student account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/sirp/browser/templates/loginpage.pt:39
+msgid "login_trouble2"
+msgstr ""
+
+#. Default: "Or simply forgot your student id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/sirp/browser/templates/loginpage.pt:44
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/sirp/browser/templates/loginpage.pt:5
+msgid "User Name or Id:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/sirp/browser/templates/searchpage.pt:15
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:36
+msgid "Type"
+msgstr ""
+
+#: waeup/sirp/browser/templates/searchpage.pt:9
+#: waeup/sirp/students/browser_templates/containerpage.pt:21
+msgid "Search Results"
+msgstr ""
+
+#: waeup/sirp/browser/templates/showstudentspage.pt:5
+msgid "Fullname"
+msgstr ""
+
+#: waeup/sirp/browser/templates/showstudentspage.pt:6
+msgid "Status"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:101
+#: waeup/sirp/browser/templates/studentsitelayout.pt:86
+msgid "Form Status:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:113
+#: waeup/sirp/browser/templates/studentsitelayout.pt:99
+msgid "Back to top"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:114
+#: waeup/sirp/browser/templates/studentsitelayout.pt:100
+msgid ""
+"Student Information and Registration Portal<br /> Copyright &copy; WAeUP "
+"Group, 2008-2012"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:34
+#: waeup/sirp/browser/templates/studentsitelayout.pt:39
+msgid "Contact"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:40
+#: waeup/sirp/browser/templates/studentsitelayout.pt:45
+msgid "Logout"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:60
+msgid "Student Id:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:64
+msgid "State:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/usereditformpage.pt:14
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:23
+#: waeup/sirp/students/browser_templates/change_password.pt:16
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:37
+msgid "Retype password:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:170
+msgid "Add"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:383 waeup/sirp/students/viewlets.py:155
+msgid "Send email"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:434
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:448
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:473 waeup/sirp/browser/viewlets.py:497
+msgid "Show students"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:528 waeup/sirp/browser/viewlets.py:536
+msgid "Show batch logs"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:548
+msgid "Batch processing"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:557
+msgid "Upload CSV file"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:630
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:91 waeup/sirp/browser/viewlets.py:160
+#: waeup/sirp/students/viewlets.py:218 waeup/sirp/students/viewlets.py:403
+msgid "Edit"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:105
+msgid "Accommodation Section"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:113 waeup/sirp/hostels/browser.py:123
+msgid "Manage accommodation section"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:145 waeup/sirp/hostels/browser.py:158
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:161
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:170
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:172
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:207
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:210 waeup/sirp/hostels/browser.py:236
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:211 waeup/sirp/hostels/browser.py:250
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:212 waeup/sirp/hostels/browser.py:281
+msgid "Release selected beds"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:241
+msgid ""
+"${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:275
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:299
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:304
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:316
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:68 waeup/sirp/hostels/browser.py:78
+msgid "Hostels"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:95
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:9
+#: waeup/sirp/hostels/browser_templates/containermanagepage.pt:9
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:35
+msgid "Id"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:37
+msgid "Number"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:56
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:6
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:9
+msgid "Beds"
+msgstr ""
+
+#: waeup/sirp/hostels/hostel.py:180
+msgid "unreserved"
+msgstr ""
+
+#: waeup/sirp/hostels/hostel.py:187
+msgid "reserved"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:105
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:112
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:119
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:126
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:137
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:146
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:169
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:175 waeup/sirp/students/interfaces.py:427
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:181
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:186 waeup/sirp/hostels/interfaces.py:199
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:44
+msgid "Hostel Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:49
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:55
+msgid "Hostel Name"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:61
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:67
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:73
+msgid "Reserved Beds"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:84
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:91
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:98
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:57
+msgid "Bed A"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:58
+msgid "Bed B"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:59
+msgid "Bed C"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:60
+msgid "Bed D"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:61
+msgid "Bed E"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:62
+msgid "Bed F"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:63
+msgid "Bed G"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:64
+msgid "Bed H"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:65
+msgid "Bed I"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:69
+msgid "Block A"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:70
+msgid "Block B"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:71
+msgid "Block C"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:72
+msgid "Block D"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:73
+msgid "Block E"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:74
+msgid "Block F"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:75
+msgid "Block G"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:76
+msgid "Block H"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:77
+msgid "Block I"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:78
+msgid "Block K"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:79
+msgid "Block L"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:80
+msgid "Block M"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:81
+msgid "Block N"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:82
+msgid "Block O"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:83
+msgid "Block P"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:87
+msgid "Regular Hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:88
+msgid "Blocked Hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:89
+msgid "Postgraduate Hostel"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:72
+msgid "clearance started"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:73
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:74
+msgid "cleared"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:75
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:76
+msgid "returning"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:77
+msgid "courses registered"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:78
+msgid "courses validated"
+msgstr ""
+
+#: waeup/sirp/objecthistory.py:73
+msgid "by"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:106
+msgid "Surcharge 1"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:113
+msgid "Surcharge 2"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:120
+msgid "Surcharge 3"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:133
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:140
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:147
+msgid "Response Description"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:154
+msgid "Response Payment Reference"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:161
+msgid "Response Code"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:168
+msgid "Response Card Number"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:24
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:25
+msgid "Paid"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:26
+msgid "Failed"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:30
+msgid "School Fee"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:31
+msgid "Clearance"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:32
+msgid "Bed Allocation"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:33
+msgid "Hostel Maintenance"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:34
+msgid "Transfer"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:35
+msgid "Gown"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:36
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:51
+msgid "Payment Category"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:58
+msgid "Payment Item"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:64
+msgid "Payment State"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:71
+msgid "Ticket Creation Date"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:76
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:10
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:84
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "Payment Date"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:82
+msgid "Amount Authorized"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:94
+msgid "Payment Access Code"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1003 waeup/sirp/students/browser.py:1225
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1072
+msgid "This ticket has already been paid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1085 waeup/sirp/students/browser.py:1093
+#: waeup/sirp/students/browser.py:1101
+msgid "Valid callback received. ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1155
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1167
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:117 waeup/sirp/students/viewlets.py:114
+#: waeup/sirp/students/viewlets.py:534
+msgid "Study Course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1174
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1205
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1207
+msgid "Create bed ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1215
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1219
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:123 waeup/sirp/students/viewlets.py:120
+#: waeup/sirp/students/viewlets.py:535
+msgid "Payments"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1230
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1241 waeup/sirp/students/browser.py:1537
+#: waeup/sirp/students/browser.py:1650
+msgid "Activation code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1261 waeup/sirp/students/browser.py:1389
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1266 waeup/sirp/students/browser.py:1542
+#: waeup/sirp/students/browser.py:1655
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1269 waeup/sirp/students/browser.py:1545
+#: waeup/sirp/students/browser.py:1658 waeup/sirp/students/browser.py:1917
+msgid "invalidated"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1274 waeup/sirp/students/browser.py:1549
+#: waeup/sirp/students/browser.py:1663
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1285 waeup/sirp/students/browser.py:1403
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1294
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1312
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1328
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1334
+msgid "Bed Allocation: "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1361
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1367
+msgid "Student can't be relocated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:138 waeup/sirp/students/viewlets.py:126
+msgid "Accommodation"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1410
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1429
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1441 waeup/sirp/students/viewlets.py:356
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1456 waeup/sirp/students/viewlets.py:365
+msgid "Change password"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:147
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1470
+msgid "Password changed."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1482
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1497 waeup/sirp/students/viewlets.py:389
+#: waeup/sirp/students/workflow.py:44
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1501
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1518 waeup/sirp/students/browser.py:1639
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1522
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1526
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1553
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1565
+msgid "Edit clearance data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1577 waeup/sirp/students/browser.py:1591
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1585
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1600 waeup/sirp/students/workflow.py:58
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1601
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1604
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1613
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1622
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1631 waeup/sirp/students/viewlets.py:418
+msgid "Start course registration"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1635
+msgid "Start course registration now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1671
+msgid "Course registration has been started."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1690
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1699
+msgid "Create course list now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:170
+msgid "Student Section"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1738
+msgid "Add and remove course tickets of study level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1775
+msgid "Register course list"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1779
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1825
+msgid "Change my password"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1835
+msgid "Get new login credentials"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1849
+msgid "No student record found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1857
+msgid "You have successfully changed your password for the"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:186 waeup/sirp/students/browser.py:222
+msgid "Empty search string"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1862
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1873
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1876
+msgid "Set"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1891
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1899
+msgid "Access code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1902
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1908
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1914
+msgid "Access code has already been used."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1923
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:204 waeup/sirp/students/viewlets.py:139
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:228 waeup/sirp/students/browser.py:1888
+msgid "No student found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:255 waeup/sirp/students/viewlets.py:146
+msgid "Add student"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:263
+msgid "Student record created."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:279
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:322 waeup/sirp/students/browser.py:1865
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:333
+msgid "Manage base data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:402
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:418 waeup/sirp/students/viewlets.py:104
+#: waeup/sirp/students/viewlets.py:532
+#: waeup/sirp/students/browser_templates/clearanceeditpage.pt:6
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:424
+msgid "Clearance Slip of "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:444
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:473
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:475 waeup/sirp/students/browser.py:499
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:492
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:496
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:522
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:532
+msgid "Manage personal data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:552
+msgid "${a}: Study Course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:582
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:585 waeup/sirp/students/browser.py:626
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:586 waeup/sirp/students/browser.py:612
+msgid "Add study level"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:620
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:622 waeup/sirp/students/browser.py:1708
+msgid "This level exists."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:632
+msgid "No study level selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:668
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:691
+msgid "Level Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:697
+msgid "Course List"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:706
+msgid "Course Registration Slip  "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:712
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:35
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:12
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:33
+msgid "Sem."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:715
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:38
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:36
+msgid "Dept."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:716
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:16
+msgid "Faculty"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:717
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:40
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:38
+msgid "Cred."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:718
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:39
+msgid "Mand."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:719 waeup/sirp/students/interfaces.py:370
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:40
+msgid "Score"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:747 waeup/sirp/students/browser.py:776
+#: waeup/sirp/students/browser.py:861 waeup/sirp/students/browser.py:866
+#: waeup/sirp/students/browser.py:1748 waeup/sirp/students/browser.py:1798
+msgid "Add course ticket"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:768
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:786 waeup/sirp/students/browser.py:1758
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:810 waeup/sirp/students/browser.py:832
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:814
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:816 waeup/sirp/students/browser.py:844
+msgid "Student is in the wrong state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:837
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:841
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:881 waeup/sirp/students/browser.py:1811
+msgid "The ticket exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:883 waeup/sirp/students/browser.py:1813
+msgid "Successfully added ${a}."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:904
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:920
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:947
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:992
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:995
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:11
+msgid "Bed Category"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:29
+msgid "Registration Transition:"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/sirp/students/browser_templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/clearanceeditpage.pt:8
+msgid "Scans"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:28
+msgid "Select all"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:10
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:10
+msgid "with registration number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:11
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:11
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:12
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:12
+msgid "in session"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:13
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:13
+msgid "in department"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:14
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:14
+msgid "in study mode"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:30
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:36
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:31
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:37
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:32
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:38
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:14
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:25
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:87
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:54
+msgid "State"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:33
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:39
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:44
+msgid "Full Name"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:5
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:5
+msgid "for students"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:8
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:8
+msgid "with id"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:9
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:9
+msgid "with name"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:31
+msgid "Semester:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:39
+msgid "Credits:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:47
+msgid "Passmark:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/imageupload.pt:20
+#: waeup/sirp/students/browser_templates/fileupload.pt:27
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:25
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:11
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:85
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:52
+msgid "Category"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:12
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:86
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:53
+msgid "Item"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:8
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:82
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:49
+msgid "Payment Id"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:83
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:50
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/setpassword.pt:14
+msgid "Access Code:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/setpassword.pt:32
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/sirp/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/sirp/students/browser_templates/setpassword.pt:5
+msgid "Registration Number:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:6
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:9
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:52
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:30
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:57
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:35
+msgid "Level Code"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:58
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:36
+msgid "Level Title"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:71
+msgid "There are no levels registered yet."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveladdpage.pt:4
+msgid "Text to explain course registration procedure."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:15
+msgid "Department"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:6
+msgid "Course Tickets (Total Credits: ${DYNAMIC_CONTENT})"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:39
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:37
+msgid "Fact."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:43
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:19
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:41
+msgid "CO"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:44
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:42
+msgid "Auto"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:6
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:9
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:30
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:28
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:18
+msgid "Total Credits:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:64
+msgid "The're no tickets added yet."
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:176 waeup/sirp/students/interfaces.py:255
+#: waeup/sirp/students/interfaces.py:483
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:183 waeup/sirp/students/interfaces.py:265
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:191
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:198
+msgid "Email"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:218
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:223
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:231
+msgid "Nationality"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:241
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:275
+#: waeup/sirp/browser/templates/showstudentspage.pt:7
+msgid "Certificate"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:282
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:290
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:299
+#: waeup/sirp/browser/templates/showstudentspage.pt:8
+msgid "Current Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:307
+#: waeup/sirp/browser/templates/showstudentspage.pt:9
+msgid "Current Level"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:315
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:322
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:337 waeup/sirp/students/interfaces.py:434
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:8
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:13
+msgid "Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:344
+msgid "Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:363
+#: waeup/sirp/browser/templates/certificatepage.pt:30
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:33
+msgid "Mandatory"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:377
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:384
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:403
+#: waeup/sirp/university/interfaces.py:299
+#: waeup/sirp/browser/templates/certificatepage.pt:29
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:32
+msgid "Course"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:420
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:10
+msgid "Bed Coordinates"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:442
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:448
+msgid "Booking Activation Code"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/utils.py:163
+msgid "<font size=10>${a} Page ${b} of ${c}</font>"
+msgstr ""
+
+#: waeup/sirp/students/utils.py:276 waeup/sirp/students/viewlets.py:71
+#: waeup/sirp/students/viewlets.py:99
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:6
+msgid "Base Data"
+msgstr ""
+
+#: waeup/sirp/students/utils.py:307
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:109 waeup/sirp/students/viewlets.py:533
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:131 waeup/sirp/students/viewlets.py:537
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:27
+msgid "History"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:179 waeup/sirp/students/workflow.py:72
+msgid "Clear student"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:194
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:210
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:235
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:251 waeup/sirp/students/workflow.py:128
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:267
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:330
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:339
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:348
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:374
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:432
+msgid "Add course list"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:450
+msgid "Add and remove courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:536
+msgid "Accommodation Data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:548
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:561
+msgid "File name has no extension."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:564
+msgid "File name contains more than one dot."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:571
+msgid "${a} file extension expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:577
+msgid "Only the following extension are allowed: ${a}"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:583
+msgid "Uploaded file is too big."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:590
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:608
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:10
+#: waeup/sirp/browser/templates/datacenterpage.pt:14
+#: waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt:11
+msgid "File"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:609
+msgid "Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:632
+msgid "Upload new file"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:633
+msgid "Delete attachment"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:663
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:677
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:8
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:688
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:703 waeup/sirp/students/viewlets.py:714
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:704 waeup/sirp/students/viewlets.py:715
+msgid "Birth Certificate Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:724 waeup/sirp/students/viewlets.py:735
+msgid "Acceptance Letter"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:725 waeup/sirp/students/viewlets.py:736
+msgid "Acceptance Letter Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:85
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:135
+msgid "Male"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:137
+msgid "Female"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:77
+msgid "Error: level id ${value} out of range"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:88
+msgid "${title} 2nd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:91
+msgid "${title} 3rd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:94
+msgid "${title} 1st spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:96
+msgid "${title} on 1st probation"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:98
+msgid "${title} on 2nd probation"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:107
+msgid "Reset to returning"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:108
+msgid "Student record reset to 'returning'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:114
+msgid "Register courses"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:115
+msgid "Courses registered"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:121 waeup/sirp/students/workflow.py:135
+msgid "Reset to paid"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:122 waeup/sirp/students/workflow.py:136
+msgid "Student record reset to 'paid'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:129
+msgid "Courses validated"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:142
+msgid "Return"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:143
+msgid "Returned"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:149
+msgid "Reset to validated"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:150
+msgid "Student record reset to 'validated'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:22
+msgid "Create student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:30
+msgid "Admit student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:31
+msgid "Student admitted"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:37
+msgid "Reset student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:38
+msgid "Student record reset"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:45
+msgid "Clearance started"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:51
+msgid "Reset to admitted"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:52
+msgid "Student record reset to 'admitted'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:59
+msgid "Clearance requested"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:65 waeup/sirp/students/workflow.py:79
+msgid "Reset to clearance"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:66 waeup/sirp/students/workflow.py:80
+msgid "Student record reset to 'clearance'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:73
+msgid "Cleared"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:86 waeup/sirp/students/workflow.py:100
+msgid "Pay school fee"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:87 waeup/sirp/students/workflow.py:101
+msgid "School fee paid"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:93
+msgid "Reset to cleared"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:94
+msgid "Student record reset to 'cleared'"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:146
+msgid "Title of course"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:152
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:17
+msgid "Credits"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:158
+msgid "Passmark"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:164
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:180
+msgid "Enter unique course code which will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:205
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:212
+msgid "Start Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:219
+msgid "End Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:226
+msgid "Application Category"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:243
+msgid "Enter unique certificate code which will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:264
+msgid "Course Referrer"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:270
+#: waeup/sirp/browser/templates/certificatepage.pt:28
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:30
+msgid "Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:277
+msgid "Is mandatory course (not elective)"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:45
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:46
+#: waeup/sirp/university/interfaces.py:94
+#: waeup/sirp/university/interfaces.py:147
+#: waeup/sirp/university/interfaces.py:200
+msgid "Unnamed"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:51
+#: waeup/sirp/university/interfaces.py:99
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:67
+#: waeup/sirp/university/interfaces.py:118
+msgid "This code will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:93
+msgid "Name of department"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:28
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:29
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:30
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:31
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:32
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:33
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:34
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:35
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:36
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/sirp/utils/utils.py:119
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/sirp/utils/utils.py:164
+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"
+"Login page: ${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 ""
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/fr/LC_MESSAGES/waeup.sirp.po
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/fr/LC_MESSAGES/waeup.sirp.po	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/fr/LC_MESSAGES/waeup.sirp.po	(revision 7760)
@@ -0,0 +1,3570 @@
+# #############################################################################
+#
+# 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.SIRP\n"
+"POT-Creation-Date: Fri Mar  2 12:58:47 2012\n"
+"PO-Revision-Date: 2012-02-22 11:28+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/sirp/accesscodes/browser.py:100 waeup/sirp/accesscodes/browser.py:113
+#: waeup/sirp/applicants/browser.py:80 waeup/sirp/applicants/browser.py:130
+#: waeup/sirp/applicants/browser.py:182 waeup/sirp/applicants/browser.py:254
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:338
+#: waeup/sirp/browser/pages.py:503 waeup/sirp/browser/pages.py:708
+#: waeup/sirp/browser/pages.py:735 waeup/sirp/browser/pages.py:758
+#: waeup/sirp/browser/pages.py:805 waeup/sirp/browser/pages.py:835
+#: waeup/sirp/browser/pages.py:955 waeup/sirp/browser/pages.py:1183
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1298
+#: waeup/sirp/browser/pages.py:1325 waeup/sirp/browser/pages.py:1353
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1400
+#: waeup/sirp/browser/pages.py:1441 waeup/sirp/browser/pages.py:1520
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1523
+#: waeup/sirp/browser/pages.py:1601 waeup/sirp/browser/pages.py:1646
+#: waeup/sirp/browser/pages.py:1684 waeup/sirp/browser/pages.py:1728
+#: waeup/sirp/browser/pages.py:1767 waeup/sirp/browser/pages.py:1769
+#: waeup/sirp/browser/pages.py:1804 waeup/sirp/browser/pages.py:1855
+#: waeup/sirp/browser/pages.py:1907 waeup/sirp/students/browser.py:584
+#: waeup/sirp/students/browser.py:585 waeup/sirp/students/browser.py:746
+#: waeup/sirp/students/browser.py:748 waeup/sirp/students/browser.py:888
+msgid "Cancel"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:102
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:112
+msgid "Reimport"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:115
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:120
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:126
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:136
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:139
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:152
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:153 waeup/sirp/browser/pages.py:592
+#: waeup/sirp/browser/pages.py:1142 waeup/sirp/browser/viewlets.py:190
+#: waeup/sirp/students/browser.py:171 waeup/sirp/students/browser.py:205
+msgid "Search"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:154
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:155
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:180 waeup/sirp/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:182
+msgid "${a} disabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:186
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:190
+msgid "re-enabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:192
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:196
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:235
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:244
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:252
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:261
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:43
+msgid "Archive"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:44
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:46 waeup/sirp/accesscodes/browser.py:206
+#: waeup/sirp/accesscodes/browser.py:226
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:53
+msgid "No batch selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:61
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:68
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:77
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:83
+msgid "Create batch"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:91
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:93
+msgid "Data written to ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:10
+msgid "Entries/(invalidated)"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:11
+msgid "Cost"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:12
+msgid "Date of Creation"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:13
+msgid "Creator"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:2
+msgid "The following batches are available:"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:39
+msgid "No batches yet"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:9
+msgid "Prefix"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt:23
+msgid "No import batches available"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:23
+msgid "Serial"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:24
+msgid "AC"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:26
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:38
+msgid "Owner"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:5
+msgid "for access codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:7
+msgid "with PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:8
+msgid "with serial"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:9
+msgid "with message term"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:29
+msgid "Serial number inside batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:32
+msgid "Prefix inside batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:35
+msgid "Batch number"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:38
+msgid "Random part of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:41
+#: waeup/sirp/accesscodes/interfaces.py:80
+msgid "Cost of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:45
+msgid "Workflow state"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:48
+msgid "Complete title of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:51
+msgid "Purchaser"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:54
+msgid "The history of access code as lines"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:63
+msgid "Creation date"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:66
+msgid "Batch creator"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:69
+msgid "Batch prefix"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:72
+msgid "Batch number (1-3 digits)"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:76
+msgid "Number of access codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:84
+msgid "Number of disabled access codes inside the batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:89
+msgid "Number of used access codes inside the batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:102
+msgid "Disable used PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:109
+msgid "Reenable disabled PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:37 waeup/sirp/applicants/workflow.py:37
+msgid "initialized"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:38
+msgid "used"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:81
+msgid "Initialize PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:88
+msgid "Use PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:95
+msgid "Disable unused PIN"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:117 waeup/sirp/applicants/browser.py:325
+#: waeup/sirp/applicants/browser.py:745
+msgid "Could not delete:"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:120 waeup/sirp/applicants/browser.py:328
+#: waeup/sirp/applicants/browser.py:748 waeup/sirp/browser/pages.py:134
+#: waeup/sirp/students/browser.py:243 waeup/sirp/students/browser.py:642
+#: waeup/sirp/students/browser.py:796 waeup/sirp/students/browser.py:974
+#: waeup/sirp/students/browser.py:1184 waeup/sirp/students/browser.py:1770
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:169
+msgid ""
+"An applicants container for the same application type and entrance year "
+"exists already in the database."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:178
+msgid "Added:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:190 waeup/sirp/applicants/viewlets.py:44
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:12
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:36
+msgid "Applicants"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:254 waeup/sirp/applicants/browser.py:301
+#: waeup/sirp/applicants/browser.py:654 waeup/sirp/applicants/browser.py:688
+#: waeup/sirp/applicants/browser.py:791 waeup/sirp/applicants/browser.py:794
+#: waeup/sirp/applicants/browser.py:838 waeup/sirp/browser/pages.py:476
+#: waeup/sirp/browser/pages.py:631 waeup/sirp/browser/pages.py:654
+#: waeup/sirp/browser/pages.py:728 waeup/sirp/browser/pages.py:1181
+#: waeup/sirp/browser/pages.py:1353 waeup/sirp/browser/pages.py:1394
+#: waeup/sirp/browser/pages.py:1520 waeup/sirp/browser/pages.py:1573
+#: waeup/sirp/browser/pages.py:1716 waeup/sirp/browser/pages.py:1767
+#: waeup/sirp/browser/pages.py:1788 waeup/sirp/hostels/browser.py:209
+#: waeup/sirp/hostels/browser.py:231 waeup/sirp/hostels/browser.py:319
+#: waeup/sirp/students/browser.py:359 waeup/sirp/students/browser.py:458
+#: waeup/sirp/students/browser.py:535 waeup/sirp/students/browser.py:584
+#: waeup/sirp/students/browser.py:600 waeup/sirp/students/browser.py:746
+#: waeup/sirp/students/browser.py:771 waeup/sirp/students/browser.py:1444
+#: waeup/sirp/students/browser.py:1459 waeup/sirp/students/browser.py:1574
+msgid "Save"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:333
+#: waeup/sirp/applicants/browser.py:361
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:265 waeup/sirp/applicants/viewlets.py:95
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:305 waeup/sirp/applicants/browser.py:717
+#: waeup/sirp/browser/pages.py:1397 waeup/sirp/browser/pages.py:1719
+#: waeup/sirp/browser/pages.py:1791 waeup/sirp/browser/pages.py:1903
+#: waeup/sirp/hostels/browser.py:54 waeup/sirp/students/browser.py:80
+#: waeup/sirp/students/browser.py:385
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:314
+msgid "No applicant selected!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:364
+msgid "Create application record"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:369
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:382
+msgid "Applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:397 waeup/sirp/students/browser.py:285
+msgid "set"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:398 waeup/sirp/students/browser.py:286
+msgid "unset"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:403
+msgid "${a} Application Record ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:454
+msgid "Session configuration object is not available."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:461 waeup/sirp/students/browser.py:1023
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:474 waeup/sirp/students/browser.py:1037
+msgid "Payment ticket created."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:501 waeup/sirp/students/browser.py:1056
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:529 waeup/sirp/students/browser.py:1105
+msgid "Valid callback received."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:550
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:556
+msgid "Online Payment Receipt"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:562
+msgid "Ticket not yet paid."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:627
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:632
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:64
+msgid "Application Section"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:654 waeup/sirp/applicants/browser.py:794
+#: waeup/sirp/applicants/browser.py:846
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:655 waeup/sirp/applicants/browser.py:758
+#: waeup/sirp/applicants/browser.py:792 waeup/sirp/applicants/browser.py:827
+#: waeup/sirp/students/browser.py:980
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:655 waeup/sirp/applicants/browser.py:763
+#: waeup/sirp/applicants/browser.py:792 waeup/sirp/applicants/browser.py:795
+#: waeup/sirp/applicants/browser.py:832 waeup/sirp/students/browser.py:748
+#: waeup/sirp/students/browser.py:780 waeup/sirp/students/browser.py:956
+#: waeup/sirp/students/browser.py:1752
+msgid "Remove selected tickets"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:674
+msgid "${a} Application Form ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:685 waeup/sirp/students/browser.py:356
+msgid "No transition"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:732 waeup/sirp/students/browser.py:962
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:76 waeup/sirp/applicants/viewlets.py:88
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:79 waeup/sirp/applicants/browser.py:105
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:308
+#: waeup/sirp/browser/pages.py:634 waeup/sirp/browser/pages.py:675
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1288
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1389
+#: waeup/sirp/hostels/browser.py:131 waeup/sirp/students/browser.py:206
+#: waeup/sirp/students/browser.py:1151 waeup/sirp/students/browser.py:1164
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:79 waeup/sirp/applicants/browser.py:125
+#: waeup/sirp/applicants/browser.py:148 waeup/sirp/applicants/browser.py:160
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:805 waeup/sirp/students/browser.py:88
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:81 waeup/sirp/applicants/browser.py:139
+#: waeup/sirp/applicants/browser.py:256 waeup/sirp/applicants/browser.py:347
+#: waeup/sirp/browser/pages.py:1355 waeup/sirp/browser/pages.py:1414
+#: waeup/sirp/browser/pages.py:1524 waeup/sirp/browser/pages.py:1610
+#: waeup/sirp/browser/pages.py:1770 waeup/sirp/browser/pages.py:1827
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:819
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:82 waeup/sirp/applicants/browser.py:135
+#: waeup/sirp/applicants/browser.py:257 waeup/sirp/applicants/browser.py:343
+#: waeup/sirp/browser/pages.py:1356 waeup/sirp/browser/pages.py:1410
+#: waeup/sirp/browser/pages.py:1525 waeup/sirp/browser/pages.py:1606
+#: waeup/sirp/browser/pages.py:1771 waeup/sirp/browser/pages.py:1823
+msgid "Add local role"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:821
+msgid "Passport picture confirmation box not ticked."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:83
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:858 waeup/sirp/students/browser.py:1619
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:863
+msgid "Form has been submitted."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:898
+msgid "Register for ${a} Application"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:904
+msgid "Application has not yet started."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:909
+msgid "Application has ended."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:918
+msgid "Get login credentials"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:933
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:940
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:949
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:18
+msgid "Photograph"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:43
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:59
+msgid ""
+"I confirm that the Passport Photograph uploaded on this form is a true "
+"picture of me."
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:74
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:43
+msgid "Acceptance Fee Payment Tickets"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:11
+msgid "Thanks for using SIRP!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:3
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/sirp/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/sirp/applicants/browser_templates/applicantregister.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:42
+msgid "Application Number"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:46
+msgid "First Choice"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:48
+msgid "Application State"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:4
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:23
+msgid "Year"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:7
+msgid "Application Period"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:197 waeup/sirp/students/browser.py:713
+#: waeup/sirp/university/interfaces.py:38
+#: waeup/sirp/university/interfaces.py:66
+#: waeup/sirp/university/interfaces.py:86
+#: waeup/sirp/university/interfaces.py:117
+#: waeup/sirp/university/interfaces.py:139
+#: waeup/sirp/university/interfaces.py:179
+#: waeup/sirp/university/interfaces.py:192
+#: waeup/sirp/university/interfaces.py:241
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:39
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:68
+#: waeup/sirp/browser/templates/facultypage.pt:8
+#: waeup/sirp/browser/templates/departmentpage.pt:15
+#: waeup/sirp/browser/templates/departmentpage.pt:39
+#: waeup/sirp/browser/templates/searchpage.pt:13
+#: waeup/sirp/browser/templates/facultymanagepage.pt:41
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:36
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:13
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:34
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:5
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:25
+msgid "Code"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:204 waeup/sirp/students/browser.py:714
+#: waeup/sirp/university/interfaces.py:199
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:41
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:70
+#: waeup/sirp/browser/templates/facultypage.pt:9
+#: waeup/sirp/browser/templates/departmentpage.pt:16
+#: waeup/sirp/browser/templates/departmentpage.pt:40
+#: waeup/sirp/browser/templates/searchpage.pt:14
+#: waeup/sirp/browser/templates/certificatepage.pt:30
+#: waeup/sirp/browser/templates/facultymanagepage.pt:43
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:32
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:37
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:35
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:6
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:27
+msgid "Title"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:211
+#: waeup/sirp/applicants/interfaces.py:324
+msgid "Application Target"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:219
+msgid "Entry Level"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:226
+#: waeup/sirp/applicants/interfaces.py:332
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:234
+#: waeup/sirp/applicants/interfaces.py:340
+msgid "Applicants Container Type"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:250
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:257
+msgid "Human readable description in reST format"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:273
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:279
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:285
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:368
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:373
+msgid "JAMB Registration Number"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:385
+#: waeup/sirp/students/interfaces.py:154
+msgid "First Name"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:389
+#: waeup/sirp/students/interfaces.py:159
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:393
+#: waeup/sirp/students/interfaces.py:164
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:397
+#: waeup/sirp/students/interfaces.py:213
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:402
+msgid "State/LGA"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:407
+#: waeup/sirp/students/interfaces.py:169
+msgid "Sex"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:413
+#: waeup/sirp/students/interfaces.py:490
+msgid "Email Address"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:418
+#: waeup/sirp/students/interfaces.py:203
+msgid "Phone"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:423
+#: waeup/sirp/applicants/interfaces.py:503
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:428
+#: waeup/sirp/applicants/interfaces.py:508
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:437
+#: waeup/sirp/applicants/interfaces.py:513
+msgid "Screening Score"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:441
+#: waeup/sirp/applicants/interfaces.py:518
+msgid "Screening Venue"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:445
+#: waeup/sirp/applicants/interfaces.py:523
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:451
+#: waeup/sirp/applicants/interfaces.py:530
+msgid "Notice"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:473
+#: waeup/sirp/students/interfaces.py:149
+#: waeup/sirp/browser/templates/showstudentspage.pt:4
+#: waeup/sirp/students/browser_templates/containerpage.pt:29
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:35
+msgid "Student Id"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:478
+msgid "Form locked"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:557
+#: waeup/sirp/students/interfaces.py:469
+msgid "Payment Session"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:110
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:23
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:121
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:15
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:182
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:26
+msgid "Admitted Course of Study:"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/pdf.py:200
+msgid ""
+"<font size=10>Proceed to the login page of the portal and enter your new "
+"credentials: user name= ${a}, password = ${b}.</font>"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:206
+msgid "<font size=10>Change your password when you have  logged in.</font>"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:57
+msgid "Application Record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:103
+msgid "Register for application"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:111
+msgid "View application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:119
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:127
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:143
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:151 waeup/sirp/applicants/workflow.py:99
+#: waeup/sirp/students/browser.py:258
+msgid "Create student record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:168 waeup/sirp/students/viewlets.py:300
+msgid "Download payment receipt"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:183 waeup/sirp/students/viewlets.py:315
+msgid "Request callback"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:57
+msgid "Application"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:73 waeup/sirp/students/viewlets.py:512
+msgid "My Data"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:100 waeup/sirp/students/workflow.py:25
+msgid "Student record created"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:106 waeup/sirp/applicants/workflow.py:113
+#: waeup/sirp/applicants/workflow.py:120 waeup/sirp/applicants/workflow.py:127
+msgid "Reset application to started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:107 waeup/sirp/applicants/workflow.py:114
+#: waeup/sirp/applicants/workflow.py:121 waeup/sirp/applicants/workflow.py:128
+#: waeup/sirp/applicants/workflow.py:134
+msgid "Application reset"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:133
+msgid "Reset application to paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:38
+msgid "started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:39
+msgid "paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:40
+msgid "submitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:41 waeup/sirp/interfaces.py:71
+msgid "admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:42
+msgid "not admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:43 waeup/sirp/interfaces.py:70
+msgid "created"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:49
+msgid "Initialize application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:52
+msgid "Application initialized"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:57
+msgid "Start application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:58
+msgid "Application started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:64
+msgid "Pay acceptance fee"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:65
+msgid "Fee paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:71
+msgid "Submit application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:72
+msgid "Application submitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:78
+msgid "Admit applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:79
+msgid "Applicant admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:85 waeup/sirp/applicants/workflow.py:92
+msgid "Refuse application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:86 waeup/sirp/applicants/workflow.py:93
+msgid "Application refused"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:104
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:116 waeup/sirp/browser/viewlets.py:615
+msgid "Academics"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:123 waeup/sirp/browser/pages.py:309
+msgid "Administration"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:130 waeup/sirp/browser/viewlets.py:360
+#: waeup/sirp/browser/viewlets.py:666
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:148 waeup/sirp/browser/pages.py:386
+#: waeup/sirp/browser/viewlets.py:375 waeup/sirp/browser/viewlets.py:676
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:155 waeup/sirp/browser/pages.py:748
+#: waeup/sirp/browser/pages.py:1140 waeup/sirp/browser/viewlets.py:393
+#: waeup/sirp/browser/viewlets.py:685
+msgid "Data Center"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:90
+msgid "Home"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1016
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:103
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1063
+msgid "View processing log"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1064
+msgid "Back to data center"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1108
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1110
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1118
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1120
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1121
+msgid "Show"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:113 waeup/sirp/hostels/browser.py:256
+#: waeup/sirp/hostels/browser.py:287 waeup/sirp/students/browser.py:230
+msgid "No item selected."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1143
+msgid "Back"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1151
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1179
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1199
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1202
+msgid "Given storage path cannot be used."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1203
+msgid "Error: ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1206
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1265
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1293
+#: waeup/sirp/browser/pages.py:1310 waeup/sirp/browser/pages.py:1314
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1278
+msgid "Faculties"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1282 waeup/sirp/browser/viewlets.py:441
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1321
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1342 waeup/sirp/browser/pages.py:1352
+msgid "Departments"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1405
+#: waeup/sirp/browser/pages.py:1424 waeup/sirp/browser/pages.py:1428
+msgid "Add department"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1362 waeup/sirp/browser/viewlets.py:457
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1435
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1438
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1455
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1485 waeup/sirp/browser/pages.py:1504
+#: waeup/sirp/students/browser.py:96 waeup/sirp/students/viewlets.py:472
+msgid "Students"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:152 waeup/sirp/browser/viewlets.py:322
+#: waeup/sirp/browser/templates/staffsitelayout.pt:28
+#: waeup/sirp/browser/templates/studentsitelayout.pt:33
+msgid "Login"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1579
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1591
+#: waeup/sirp/browser/pages.py:1620 waeup/sirp/browser/pages.py:1624
+msgid "Add course"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1522 waeup/sirp/browser/pages.py:1585
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1522 waeup/sirp/browser/pages.py:1596
+#: waeup/sirp/browser/pages.py:1657 waeup/sirp/browser/pages.py:1661
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1531 waeup/sirp/browser/viewlets.py:464
+msgid "Manage department"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:160
+msgid "You logged in."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1636
+msgid "A course with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1642
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1673
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1679
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1711 waeup/sirp/browser/viewlets.py:481
+msgid "Edit course"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1761
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1768 waeup/sirp/browser/pages.py:1799
+#: waeup/sirp/browser/pages.py:1840 waeup/sirp/browser/pages.py:1842
+msgid "Add course referrer"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1769 waeup/sirp/browser/pages.py:1794
+msgid "Remove selected course referrers"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1775 waeup/sirp/browser/viewlets.py:488
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:178
+msgid "You entered wrong credentials."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1847
+msgid "The chosen course referrer is already part of this certificate."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1851
+msgid "Course referrer ${a}_${b} added."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1888 waeup/sirp/browser/viewlets.py:505
+msgid "Edit course referrer"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1891
+msgid "Save and return"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:192
+msgid "You have been logged out. Thanks for using WAeUP SIRP!"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:228
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:234 waeup/sirp/browser/pages.py:519
+msgid "Send message now"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:253 waeup/sirp/browser/pages.py:272
+#: waeup/sirp/browser/pages.py:536 waeup/sirp/students/browser.py:320
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:263
+msgid "Send now"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:274
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:299
+msgid "<h1>Welcome to WAeUP.SIRP</h1>"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:387 waeup/sirp/hostels/browser.py:195
+#: waeup/sirp/students/viewlets.py:163 waeup/sirp/students/viewlets.py:171
+#: waeup/sirp/students/viewlets.py:226 waeup/sirp/students/viewlets.py:243
+#: waeup/sirp/students/viewlets.py:283
+msgid "Manage"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:388 waeup/sirp/browser/viewlets.py:180
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:395
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:426 waeup/sirp/browser/pages.py:428
+#: waeup/sirp/browser/viewlets.py:515
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:450
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:469
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:500
+msgid "User settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:517 waeup/sirp/students/browser.py:301
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:546 waeup/sirp/browser/viewlets.py:400
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:559 waeup/sirp/browser/viewlets.py:415
+msgid "My Roles"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:590
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:617
+msgid "View portal configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:630
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:631 waeup/sirp/browser/pages.py:680
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:633 waeup/sirp/browser/pages.py:661
+#: waeup/sirp/browser/pages.py:692
+msgid "Add session configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:658 waeup/sirp/browser/pages.py:731
+msgid "Settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:672
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:683
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:696
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:703
+msgid "The session chosen already exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:725
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:755
+msgid "Upload file"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:757
+msgid "Upload"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:803 waeup/sirp/browser/pages.py:833
+#: waeup/sirp/browser/pages.py:953 waeup/sirp/browser/pages.py:1061
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:817 waeup/sirp/browser/pages.py:918
+#: waeup/sirp/browser/pages.py:1037
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:83
+msgid "No local role selected."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:836
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:837
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:897
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:905
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:930
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:956 waeup/sirp/browser/pages.py:1182
+msgid "Reset"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:957
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:958
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:959
+msgid "Perform import"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:10
+msgid "Course Code:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:14
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:7
+msgid "Course Title:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:18
+msgid "Provided by:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:26
+msgid "Level:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:30
+msgid "Is mandatory course (not elective):"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:6
+msgid "Code:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:22
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:6
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:26
+msgid "Course Referrers"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:28
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:31
+msgid "Semester"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:29
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:31
+msgid "Referrer"
+msgstr ""
+
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr ""
+
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:8
+msgid "Session Configurations"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:14
+msgid "Datasets"
+msgstr ""
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:43
+msgid "Mode:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:12
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:6
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:4
+msgid "File:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:18
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:9
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:32
+msgid "Processor:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:24
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:13
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:44
+msgid "change to:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:12
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:16
+msgid "Date"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:14
+#: waeup/sirp/browser/templates/datacenterpage.pt:15
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:12
+msgid "Overwrite contents in new location?"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:3
+#: waeup/sirp/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:7
+msgid "Copy existing data to new storage?"
+msgstr ""
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterpage.pt:16
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:10
+#: waeup/sirp/browser/templates/departmentpage.pt:3
+msgid "Courses"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:101
+#: waeup/sirp/browser/templates/facultymanagepage.pt:75
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:92
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:93
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:60
+msgid "Local Role"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:13
+#: waeup/sirp/browser/templates/departmentpage.pt:6
+msgid "Certificates"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:16
+#: waeup/sirp/browser/templates/userscontainerpage.pt:7
+#: waeup/sirp/browser/templates/facultymanagepage.pt:16
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:7
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:17
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:11
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:6
+#: waeup/sirp/browser/templates/facultymanagepage.pt:6
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:5
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:7
+msgid "Settings"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:97
+#: waeup/sirp/browser/templates/userscontainerpage.pt:4
+#: waeup/sirp/browser/templates/facultymanagepage.pt:71
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:88
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:89
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:56
+msgid "User Id"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:99
+#: waeup/sirp/browser/templates/userscontainerpage.pt:5
+#: waeup/sirp/browser/templates/facultymanagepage.pt:73
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:31
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:90
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:91
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:58
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:10
+#: waeup/sirp/hostels/browser_templates/containermanagepage.pt:11
+msgid "Name"
+msgstr ""
+
+#: waeup/sirp/browser/templates/facultypage.pt:1
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:1
+msgid "There no subobjects registered yet."
+msgstr ""
+
+#: waeup/sirp/browser/templates/loginpage.pt:13
+#: waeup/sirp/browser/templates/usereditformpage.pt:8
+#: waeup/sirp/students/browser_templates/basepage.pt:14
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:17
+#: waeup/sirp/students/browser_templates/change_password.pt:10
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:31
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:34
+msgid "Password:"
+msgstr ""
+
+#: waeup/sirp/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/sirp/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 Access Code (PWD) and inititialize your student account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/sirp/browser/templates/loginpage.pt:39
+msgid "login_trouble2"
+msgstr ""
+
+#. Default: "Or simply forgot your student id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/sirp/browser/templates/loginpage.pt:44
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/sirp/browser/templates/loginpage.pt:5
+msgid "User Name or Id:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/sirp/browser/templates/searchpage.pt:15
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:36
+msgid "Type"
+msgstr ""
+
+#: waeup/sirp/browser/templates/searchpage.pt:9
+#: waeup/sirp/students/browser_templates/containerpage.pt:21
+msgid "Search Results"
+msgstr ""
+
+#: waeup/sirp/browser/templates/showstudentspage.pt:5
+msgid "Fullname"
+msgstr ""
+
+#: waeup/sirp/browser/templates/showstudentspage.pt:6
+msgid "Status"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:101
+#: waeup/sirp/browser/templates/studentsitelayout.pt:86
+msgid "Form Status:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:113
+#: waeup/sirp/browser/templates/studentsitelayout.pt:99
+msgid "Back to top"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:114
+#: waeup/sirp/browser/templates/studentsitelayout.pt:100
+msgid ""
+"Student Information and Registration Portal<br /> Copyright &copy; WAeUP "
+"Group, 2008-2012"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:34
+#: waeup/sirp/browser/templates/studentsitelayout.pt:39
+msgid "Contact"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:40
+#: waeup/sirp/browser/templates/studentsitelayout.pt:45
+msgid "Logout"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:60
+msgid "Student Id:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:64
+msgid "State:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/usereditformpage.pt:14
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:23
+#: waeup/sirp/students/browser_templates/change_password.pt:16
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:37
+msgid "Retype password:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:170
+msgid "Add"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:383 waeup/sirp/students/viewlets.py:155
+msgid "Send email"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:434
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:448
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:473 waeup/sirp/browser/viewlets.py:497
+msgid "Show students"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:528 waeup/sirp/browser/viewlets.py:536
+msgid "Show batch logs"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:548
+msgid "Batch processing"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:557
+msgid "Upload CSV file"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:630
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:91 waeup/sirp/browser/viewlets.py:160
+#: waeup/sirp/students/viewlets.py:218 waeup/sirp/students/viewlets.py:403
+msgid "Edit"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:105
+msgid "Accommodation Section"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:113 waeup/sirp/hostels/browser.py:123
+msgid "Manage accommodation section"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:145 waeup/sirp/hostels/browser.py:158
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:161
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:170
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:172
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:207
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:210 waeup/sirp/hostels/browser.py:236
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:211 waeup/sirp/hostels/browser.py:250
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:212 waeup/sirp/hostels/browser.py:281
+msgid "Release selected beds"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:241
+msgid ""
+"${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:275
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:299
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:304
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:316
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:68 waeup/sirp/hostels/browser.py:78
+msgid "Hostels"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:95
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:9
+#: waeup/sirp/hostels/browser_templates/containermanagepage.pt:9
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:35
+msgid "Id"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:37
+msgid "Number"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:56
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:6
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:9
+msgid "Beds"
+msgstr ""
+
+#: waeup/sirp/hostels/hostel.py:180
+msgid "unreserved"
+msgstr ""
+
+#: waeup/sirp/hostels/hostel.py:187
+msgid "reserved"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:105
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:112
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:119
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:126
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:137
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:146
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:169
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:175 waeup/sirp/students/interfaces.py:427
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:181
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:186 waeup/sirp/hostels/interfaces.py:199
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:44
+msgid "Hostel Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:49
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:55
+msgid "Hostel Name"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:61
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:67
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:73
+msgid "Reserved Beds"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:84
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:91
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:98
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:57
+msgid "Bed A"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:58
+msgid "Bed B"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:59
+msgid "Bed C"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:60
+msgid "Bed D"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:61
+msgid "Bed E"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:62
+msgid "Bed F"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:63
+msgid "Bed G"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:64
+msgid "Bed H"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:65
+msgid "Bed I"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:69
+msgid "Block A"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:70
+msgid "Block B"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:71
+msgid "Block C"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:72
+msgid "Block D"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:73
+msgid "Block E"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:74
+msgid "Block F"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:75
+msgid "Block G"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:76
+msgid "Block H"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:77
+msgid "Block I"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:78
+msgid "Block K"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:79
+msgid "Block L"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:80
+msgid "Block M"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:81
+msgid "Block N"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:82
+msgid "Block O"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:83
+msgid "Block P"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:87
+msgid "Regular Hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:88
+msgid "Blocked Hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:89
+msgid "Postgraduate Hostel"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:72
+msgid "clearance started"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:73
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:74
+msgid "cleared"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:75
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:76
+msgid "returning"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:77
+msgid "courses registered"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:78
+msgid "courses validated"
+msgstr ""
+
+#: waeup/sirp/objecthistory.py:73
+msgid "by"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:106
+msgid "Surcharge 1"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:113
+msgid "Surcharge 2"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:120
+msgid "Surcharge 3"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:133
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:140
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:147
+msgid "Response Description"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:154
+msgid "Response Payment Reference"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:161
+msgid "Response Code"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:168
+msgid "Response Card Number"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:24
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:25
+msgid "Paid"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:26
+msgid "Failed"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:30
+msgid "School Fee"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:31
+msgid "Clearance"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:32
+msgid "Bed Allocation"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:33
+msgid "Hostel Maintenance"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:34
+msgid "Transfer"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:35
+msgid "Gown"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:36
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:51
+msgid "Payment Category"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:58
+msgid "Payment Item"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:64
+msgid "Payment State"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:71
+msgid "Ticket Creation Date"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:76
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:10
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:84
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "Payment Date"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:82
+msgid "Amount Authorized"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:94
+msgid "Payment Access Code"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1003 waeup/sirp/students/browser.py:1225
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1072
+msgid "This ticket has already been paid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1085 waeup/sirp/students/browser.py:1093
+#: waeup/sirp/students/browser.py:1101
+msgid "Valid callback received. ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1155
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1167
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:117 waeup/sirp/students/viewlets.py:114
+#: waeup/sirp/students/viewlets.py:534
+msgid "Study Course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1174
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1205
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1207
+msgid "Create bed ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1215
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1219
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:123 waeup/sirp/students/viewlets.py:120
+#: waeup/sirp/students/viewlets.py:535
+msgid "Payments"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1230
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1241 waeup/sirp/students/browser.py:1537
+#: waeup/sirp/students/browser.py:1650
+msgid "Activation code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1261 waeup/sirp/students/browser.py:1389
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1266 waeup/sirp/students/browser.py:1542
+#: waeup/sirp/students/browser.py:1655
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1269 waeup/sirp/students/browser.py:1545
+#: waeup/sirp/students/browser.py:1658 waeup/sirp/students/browser.py:1917
+msgid "invalidated"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1274 waeup/sirp/students/browser.py:1549
+#: waeup/sirp/students/browser.py:1663
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1285 waeup/sirp/students/browser.py:1403
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1294
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1312
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1328
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1334
+msgid "Bed Allocation: "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1361
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1367
+msgid "Student can't be relocated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:138 waeup/sirp/students/viewlets.py:126
+msgid "Accommodation"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1410
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1429
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1441 waeup/sirp/students/viewlets.py:356
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1456 waeup/sirp/students/viewlets.py:365
+msgid "Change password"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:147
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1470
+msgid "Password changed."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1482
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1497 waeup/sirp/students/viewlets.py:389
+#: waeup/sirp/students/workflow.py:44
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1501
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1518 waeup/sirp/students/browser.py:1639
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1522
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1526
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1553
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1565
+msgid "Edit clearance data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1577 waeup/sirp/students/browser.py:1591
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1585
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1600 waeup/sirp/students/workflow.py:58
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1601
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1604
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1613
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1622
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1631 waeup/sirp/students/viewlets.py:418
+msgid "Start course registration"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1635
+msgid "Start course registration now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1671
+msgid "Course registration has been started."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1690
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1699
+msgid "Create course list now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:170
+msgid "Student Section"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1738
+msgid "Add and remove course tickets of study level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1775
+msgid "Register course list"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1779
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1825
+msgid "Change my password"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1835
+msgid "Get new login credentials"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1849
+msgid "No student record found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1857
+msgid "You have successfully changed your password for the"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:186 waeup/sirp/students/browser.py:222
+msgid "Empty search string"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1862
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1873
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1876
+msgid "Set"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1891
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1899
+msgid "Access code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1902
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1908
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1914
+msgid "Access code has already been used."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1923
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:204 waeup/sirp/students/viewlets.py:139
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:228 waeup/sirp/students/browser.py:1888
+msgid "No student found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:255 waeup/sirp/students/viewlets.py:146
+msgid "Add student"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:263
+msgid "Student record created."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:279
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:322 waeup/sirp/students/browser.py:1865
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:333
+msgid "Manage base data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:402
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:418 waeup/sirp/students/viewlets.py:104
+#: waeup/sirp/students/viewlets.py:532
+#: waeup/sirp/students/browser_templates/clearanceeditpage.pt:6
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:424
+msgid "Clearance Slip of "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:444
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:473
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:475 waeup/sirp/students/browser.py:499
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:492
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:496
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:522
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:532
+msgid "Manage personal data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:552
+msgid "${a}: Study Course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:582
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:585 waeup/sirp/students/browser.py:626
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:586 waeup/sirp/students/browser.py:612
+msgid "Add study level"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:620
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:622 waeup/sirp/students/browser.py:1708
+msgid "This level exists."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:632
+msgid "No study level selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:668
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:691
+msgid "Level Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:697
+msgid "Course List"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:706
+msgid "Course Registration Slip  "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:712
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:35
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:12
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:33
+msgid "Sem."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:715
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:38
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:36
+msgid "Dept."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:716
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:16
+msgid "Faculty"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:717
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:40
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:38
+msgid "Cred."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:718
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:39
+msgid "Mand."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:719 waeup/sirp/students/interfaces.py:370
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:40
+msgid "Score"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:747 waeup/sirp/students/browser.py:776
+#: waeup/sirp/students/browser.py:861 waeup/sirp/students/browser.py:866
+#: waeup/sirp/students/browser.py:1748 waeup/sirp/students/browser.py:1798
+msgid "Add course ticket"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:768
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:786 waeup/sirp/students/browser.py:1758
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:810 waeup/sirp/students/browser.py:832
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:814
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:816 waeup/sirp/students/browser.py:844
+msgid "Student is in the wrong state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:837
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:841
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:881 waeup/sirp/students/browser.py:1811
+msgid "The ticket exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:883 waeup/sirp/students/browser.py:1813
+msgid "Successfully added ${a}."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:904
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:920
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:947
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:992
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:995
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:11
+msgid "Bed Category"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:29
+msgid "Registration Transition:"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/sirp/students/browser_templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/clearanceeditpage.pt:8
+msgid "Scans"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:28
+msgid "Select all"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:10
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:10
+msgid "with registration number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:11
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:11
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:12
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:12
+msgid "in session"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:13
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:13
+msgid "in department"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:14
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:14
+msgid "in study mode"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:30
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:36
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:31
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:37
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:32
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:38
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:14
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:25
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:87
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:54
+msgid "State"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:33
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:39
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:44
+msgid "Full Name"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:5
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:5
+msgid "for students"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:8
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:8
+msgid "with id"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:9
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:9
+msgid "with name"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:31
+msgid "Semester:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:39
+msgid "Credits:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:47
+msgid "Passmark:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/imageupload.pt:20
+#: waeup/sirp/students/browser_templates/fileupload.pt:27
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:25
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:11
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:85
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:52
+msgid "Category"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:12
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:86
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:53
+msgid "Item"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:8
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:82
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:49
+msgid "Payment Id"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:83
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:50
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/setpassword.pt:14
+msgid "Access Code:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/setpassword.pt:32
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/sirp/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/sirp/students/browser_templates/setpassword.pt:5
+msgid "Registration Number:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:6
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:9
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:52
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:30
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:57
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:35
+msgid "Level Code"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:58
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:36
+msgid "Level Title"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:71
+msgid "There are no levels registered yet."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveladdpage.pt:4
+msgid "Text to explain course registration procedure."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:15
+msgid "Department"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:6
+msgid "Course Tickets (Total Credits: ${DYNAMIC_CONTENT})"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:39
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:37
+msgid "Fact."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:43
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:19
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:41
+msgid "CO"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:44
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:42
+msgid "Auto"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:6
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:9
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:30
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:28
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:18
+msgid "Total Credits:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:64
+msgid "The're no tickets added yet."
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:176 waeup/sirp/students/interfaces.py:255
+#: waeup/sirp/students/interfaces.py:483
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:183 waeup/sirp/students/interfaces.py:265
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:191
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:198
+msgid "Email"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:218
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:223
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:231
+msgid "Nationality"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:241
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:275
+#: waeup/sirp/browser/templates/showstudentspage.pt:7
+msgid "Certificate"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:282
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:290
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:299
+#: waeup/sirp/browser/templates/showstudentspage.pt:8
+msgid "Current Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:307
+#: waeup/sirp/browser/templates/showstudentspage.pt:9
+msgid "Current Level"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:315
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:322
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:337 waeup/sirp/students/interfaces.py:434
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:8
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:13
+msgid "Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:344
+msgid "Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:363
+#: waeup/sirp/browser/templates/certificatepage.pt:30
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:33
+msgid "Mandatory"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:377
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:384
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:403
+#: waeup/sirp/university/interfaces.py:299
+#: waeup/sirp/browser/templates/certificatepage.pt:29
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:32
+msgid "Course"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:420
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:10
+msgid "Bed Coordinates"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:442
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:448
+msgid "Booking Activation Code"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/utils.py:163
+msgid "<font size=10>${a} Page ${b} of ${c}</font>"
+msgstr ""
+
+#: waeup/sirp/students/utils.py:276 waeup/sirp/students/viewlets.py:71
+#: waeup/sirp/students/viewlets.py:99
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:6
+msgid "Base Data"
+msgstr ""
+
+#: waeup/sirp/students/utils.py:307
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:109 waeup/sirp/students/viewlets.py:533
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:131 waeup/sirp/students/viewlets.py:537
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:27
+msgid "History"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:179 waeup/sirp/students/workflow.py:72
+msgid "Clear student"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:194
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:210
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:235
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:251 waeup/sirp/students/workflow.py:128
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:267
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:330
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:339
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:348
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:374
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:432
+msgid "Add course list"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:450
+msgid "Add and remove courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:536
+msgid "Accommodation Data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:548
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:561
+msgid "File name has no extension."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:564
+msgid "File name contains more than one dot."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:571
+msgid "${a} file extension expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:577
+msgid "Only the following extension are allowed: ${a}"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:583
+msgid "Uploaded file is too big."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:590
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:608
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:10
+#: waeup/sirp/browser/templates/datacenterpage.pt:14
+#: waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt:11
+msgid "File"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:609
+msgid "Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:632
+msgid "Upload new file"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:633
+msgid "Delete attachment"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:663
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:677
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:8
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:688
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:703 waeup/sirp/students/viewlets.py:714
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:704 waeup/sirp/students/viewlets.py:715
+msgid "Birth Certificate Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:724 waeup/sirp/students/viewlets.py:735
+msgid "Acceptance Letter"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:725 waeup/sirp/students/viewlets.py:736
+msgid "Acceptance Letter Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:85
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:135
+msgid "Male"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:137
+msgid "Female"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:77
+msgid "Error: level id ${value} out of range"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:88
+msgid "${title} 2nd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:91
+msgid "${title} 3rd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:94
+msgid "${title} 1st spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:96
+msgid "${title} on 1st probation"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:98
+msgid "${title} on 2nd probation"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:107
+msgid "Reset to returning"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:108
+msgid "Student record reset to 'returning'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:114
+msgid "Register courses"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:115
+msgid "Courses registered"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:121 waeup/sirp/students/workflow.py:135
+msgid "Reset to paid"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:122 waeup/sirp/students/workflow.py:136
+msgid "Student record reset to 'paid'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:129
+msgid "Courses validated"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:142
+msgid "Return"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:143
+msgid "Returned"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:149
+msgid "Reset to validated"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:150
+msgid "Student record reset to 'validated'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:22
+msgid "Create student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:30
+msgid "Admit student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:31
+msgid "Student admitted"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:37
+msgid "Reset student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:38
+msgid "Student record reset"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:45
+msgid "Clearance started"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:51
+msgid "Reset to admitted"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:52
+msgid "Student record reset to 'admitted'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:59
+msgid "Clearance requested"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:65 waeup/sirp/students/workflow.py:79
+msgid "Reset to clearance"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:66 waeup/sirp/students/workflow.py:80
+msgid "Student record reset to 'clearance'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:73
+msgid "Cleared"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:86 waeup/sirp/students/workflow.py:100
+msgid "Pay school fee"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:87 waeup/sirp/students/workflow.py:101
+msgid "School fee paid"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:93
+msgid "Reset to cleared"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:94
+msgid "Student record reset to 'cleared'"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:146
+msgid "Title of course"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:152
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:17
+msgid "Credits"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:158
+msgid "Passmark"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:164
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:180
+msgid "Enter unique course code which will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:205
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:212
+msgid "Start Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:219
+msgid "End Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:226
+msgid "Application Category"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:243
+msgid "Enter unique certificate code which will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:264
+msgid "Course Referrer"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:270
+#: waeup/sirp/browser/templates/certificatepage.pt:28
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:30
+msgid "Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:277
+msgid "Is mandatory course (not elective)"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:45
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:46
+#: waeup/sirp/university/interfaces.py:94
+#: waeup/sirp/university/interfaces.py:147
+#: waeup/sirp/university/interfaces.py:200
+msgid "Unnamed"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:51
+#: waeup/sirp/university/interfaces.py:99
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:67
+#: waeup/sirp/university/interfaces.py:118
+msgid "This code will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:93
+msgid "Name of department"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:28
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:29
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:30
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:31
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:32
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:33
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:34
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:35
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:36
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/sirp/utils/utils.py:119
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/sirp/utils/utils.py:164
+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"
+"Login page: ${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 ""
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/ha/LC_MESSAGES/waeup.sirp.po
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/ha/LC_MESSAGES/waeup.sirp.po	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/ha/LC_MESSAGES/waeup.sirp.po	(revision 7760)
@@ -0,0 +1,3570 @@
+# #############################################################################
+#
+# 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.SIRP\n"
+"POT-Creation-Date: Fri Mar  2 12:58:47 2012\n"
+"PO-Revision-Date: 2012-02-29 12:21+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/sirp/accesscodes/browser.py:100 waeup/sirp/accesscodes/browser.py:113
+#: waeup/sirp/applicants/browser.py:80 waeup/sirp/applicants/browser.py:130
+#: waeup/sirp/applicants/browser.py:182 waeup/sirp/applicants/browser.py:254
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:338
+#: waeup/sirp/browser/pages.py:503 waeup/sirp/browser/pages.py:708
+#: waeup/sirp/browser/pages.py:735 waeup/sirp/browser/pages.py:758
+#: waeup/sirp/browser/pages.py:805 waeup/sirp/browser/pages.py:835
+#: waeup/sirp/browser/pages.py:955 waeup/sirp/browser/pages.py:1183
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1298
+#: waeup/sirp/browser/pages.py:1325 waeup/sirp/browser/pages.py:1353
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1400
+#: waeup/sirp/browser/pages.py:1441 waeup/sirp/browser/pages.py:1520
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1523
+#: waeup/sirp/browser/pages.py:1601 waeup/sirp/browser/pages.py:1646
+#: waeup/sirp/browser/pages.py:1684 waeup/sirp/browser/pages.py:1728
+#: waeup/sirp/browser/pages.py:1767 waeup/sirp/browser/pages.py:1769
+#: waeup/sirp/browser/pages.py:1804 waeup/sirp/browser/pages.py:1855
+#: waeup/sirp/browser/pages.py:1907 waeup/sirp/students/browser.py:584
+#: waeup/sirp/students/browser.py:585 waeup/sirp/students/browser.py:746
+#: waeup/sirp/students/browser.py:748 waeup/sirp/students/browser.py:888
+msgid "Cancel"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:102
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:112
+msgid "Reimport"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:115
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:120
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:126
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:136
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:139
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:152
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:153 waeup/sirp/browser/pages.py:592
+#: waeup/sirp/browser/pages.py:1142 waeup/sirp/browser/viewlets.py:190
+#: waeup/sirp/students/browser.py:171 waeup/sirp/students/browser.py:205
+msgid "Search"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:154
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:155
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:180 waeup/sirp/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:182
+msgid "${a} disabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:186
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:190
+msgid "re-enabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:192
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:196
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:235
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:244
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:252
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:261
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:43
+msgid "Archive"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:44
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:46 waeup/sirp/accesscodes/browser.py:206
+#: waeup/sirp/accesscodes/browser.py:226
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:53
+msgid "No batch selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:61
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:68
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:77
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:83
+msgid "Create batch"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:91
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:93
+msgid "Data written to ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:10
+msgid "Entries/(invalidated)"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:11
+msgid "Cost"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:12
+msgid "Date of Creation"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:13
+msgid "Creator"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:2
+msgid "The following batches are available:"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:39
+msgid "No batches yet"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:9
+msgid "Prefix"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt:23
+msgid "No import batches available"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:23
+msgid "Serial"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:24
+msgid "AC"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:26
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:38
+msgid "Owner"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:5
+msgid "for access codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:7
+msgid "with PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:8
+msgid "with serial"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:9
+msgid "with message term"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:29
+msgid "Serial number inside batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:32
+msgid "Prefix inside batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:35
+msgid "Batch number"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:38
+msgid "Random part of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:41
+#: waeup/sirp/accesscodes/interfaces.py:80
+msgid "Cost of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:45
+msgid "Workflow state"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:48
+msgid "Complete title of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:51
+msgid "Purchaser"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:54
+msgid "The history of access code as lines"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:63
+msgid "Creation date"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:66
+msgid "Batch creator"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:69
+msgid "Batch prefix"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:72
+msgid "Batch number (1-3 digits)"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:76
+msgid "Number of access codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:84
+msgid "Number of disabled access codes inside the batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:89
+msgid "Number of used access codes inside the batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:102
+msgid "Disable used PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:109
+msgid "Reenable disabled PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:37 waeup/sirp/applicants/workflow.py:37
+msgid "initialized"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:38
+msgid "used"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:81
+msgid "Initialize PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:88
+msgid "Use PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:95
+msgid "Disable unused PIN"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:117 waeup/sirp/applicants/browser.py:325
+#: waeup/sirp/applicants/browser.py:745
+msgid "Could not delete:"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:120 waeup/sirp/applicants/browser.py:328
+#: waeup/sirp/applicants/browser.py:748 waeup/sirp/browser/pages.py:134
+#: waeup/sirp/students/browser.py:243 waeup/sirp/students/browser.py:642
+#: waeup/sirp/students/browser.py:796 waeup/sirp/students/browser.py:974
+#: waeup/sirp/students/browser.py:1184 waeup/sirp/students/browser.py:1770
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:169
+msgid ""
+"An applicants container for the same application type and entrance year "
+"exists already in the database."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:178
+msgid "Added:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:190 waeup/sirp/applicants/viewlets.py:44
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:12
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:36
+msgid "Applicants"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:254 waeup/sirp/applicants/browser.py:301
+#: waeup/sirp/applicants/browser.py:654 waeup/sirp/applicants/browser.py:688
+#: waeup/sirp/applicants/browser.py:791 waeup/sirp/applicants/browser.py:794
+#: waeup/sirp/applicants/browser.py:838 waeup/sirp/browser/pages.py:476
+#: waeup/sirp/browser/pages.py:631 waeup/sirp/browser/pages.py:654
+#: waeup/sirp/browser/pages.py:728 waeup/sirp/browser/pages.py:1181
+#: waeup/sirp/browser/pages.py:1353 waeup/sirp/browser/pages.py:1394
+#: waeup/sirp/browser/pages.py:1520 waeup/sirp/browser/pages.py:1573
+#: waeup/sirp/browser/pages.py:1716 waeup/sirp/browser/pages.py:1767
+#: waeup/sirp/browser/pages.py:1788 waeup/sirp/hostels/browser.py:209
+#: waeup/sirp/hostels/browser.py:231 waeup/sirp/hostels/browser.py:319
+#: waeup/sirp/students/browser.py:359 waeup/sirp/students/browser.py:458
+#: waeup/sirp/students/browser.py:535 waeup/sirp/students/browser.py:584
+#: waeup/sirp/students/browser.py:600 waeup/sirp/students/browser.py:746
+#: waeup/sirp/students/browser.py:771 waeup/sirp/students/browser.py:1444
+#: waeup/sirp/students/browser.py:1459 waeup/sirp/students/browser.py:1574
+msgid "Save"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:333
+#: waeup/sirp/applicants/browser.py:361
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:265 waeup/sirp/applicants/viewlets.py:95
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:305 waeup/sirp/applicants/browser.py:717
+#: waeup/sirp/browser/pages.py:1397 waeup/sirp/browser/pages.py:1719
+#: waeup/sirp/browser/pages.py:1791 waeup/sirp/browser/pages.py:1903
+#: waeup/sirp/hostels/browser.py:54 waeup/sirp/students/browser.py:80
+#: waeup/sirp/students/browser.py:385
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:314
+msgid "No applicant selected!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:364
+msgid "Create application record"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:369
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:382
+msgid "Applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:397 waeup/sirp/students/browser.py:285
+msgid "set"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:398 waeup/sirp/students/browser.py:286
+msgid "unset"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:403
+msgid "${a} Application Record ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:454
+msgid "Session configuration object is not available."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:461 waeup/sirp/students/browser.py:1023
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:474 waeup/sirp/students/browser.py:1037
+msgid "Payment ticket created."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:501 waeup/sirp/students/browser.py:1056
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:529 waeup/sirp/students/browser.py:1105
+msgid "Valid callback received."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:550
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:556
+msgid "Online Payment Receipt"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:562
+msgid "Ticket not yet paid."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:627
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:632
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:64
+msgid "Application Section"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:654 waeup/sirp/applicants/browser.py:794
+#: waeup/sirp/applicants/browser.py:846
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:655 waeup/sirp/applicants/browser.py:758
+#: waeup/sirp/applicants/browser.py:792 waeup/sirp/applicants/browser.py:827
+#: waeup/sirp/students/browser.py:980
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:655 waeup/sirp/applicants/browser.py:763
+#: waeup/sirp/applicants/browser.py:792 waeup/sirp/applicants/browser.py:795
+#: waeup/sirp/applicants/browser.py:832 waeup/sirp/students/browser.py:748
+#: waeup/sirp/students/browser.py:780 waeup/sirp/students/browser.py:956
+#: waeup/sirp/students/browser.py:1752
+msgid "Remove selected tickets"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:674
+msgid "${a} Application Form ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:685 waeup/sirp/students/browser.py:356
+msgid "No transition"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:732 waeup/sirp/students/browser.py:962
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:76 waeup/sirp/applicants/viewlets.py:88
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:79 waeup/sirp/applicants/browser.py:105
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:308
+#: waeup/sirp/browser/pages.py:634 waeup/sirp/browser/pages.py:675
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1288
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1389
+#: waeup/sirp/hostels/browser.py:131 waeup/sirp/students/browser.py:206
+#: waeup/sirp/students/browser.py:1151 waeup/sirp/students/browser.py:1164
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:79 waeup/sirp/applicants/browser.py:125
+#: waeup/sirp/applicants/browser.py:148 waeup/sirp/applicants/browser.py:160
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:805 waeup/sirp/students/browser.py:88
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:81 waeup/sirp/applicants/browser.py:139
+#: waeup/sirp/applicants/browser.py:256 waeup/sirp/applicants/browser.py:347
+#: waeup/sirp/browser/pages.py:1355 waeup/sirp/browser/pages.py:1414
+#: waeup/sirp/browser/pages.py:1524 waeup/sirp/browser/pages.py:1610
+#: waeup/sirp/browser/pages.py:1770 waeup/sirp/browser/pages.py:1827
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:819
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:82 waeup/sirp/applicants/browser.py:135
+#: waeup/sirp/applicants/browser.py:257 waeup/sirp/applicants/browser.py:343
+#: waeup/sirp/browser/pages.py:1356 waeup/sirp/browser/pages.py:1410
+#: waeup/sirp/browser/pages.py:1525 waeup/sirp/browser/pages.py:1606
+#: waeup/sirp/browser/pages.py:1771 waeup/sirp/browser/pages.py:1823
+msgid "Add local role"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:821
+msgid "Passport picture confirmation box not ticked."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:83
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:858 waeup/sirp/students/browser.py:1619
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:863
+msgid "Form has been submitted."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:898
+msgid "Register for ${a} Application"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:904
+msgid "Application has not yet started."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:909
+msgid "Application has ended."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:918
+msgid "Get login credentials"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:933
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:940
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:949
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:18
+msgid "Photograph"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:43
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:59
+msgid ""
+"I confirm that the Passport Photograph uploaded on this form is a true "
+"picture of me."
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:74
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:43
+msgid "Acceptance Fee Payment Tickets"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:11
+msgid "Thanks for using SIRP!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:3
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/sirp/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/sirp/applicants/browser_templates/applicantregister.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:42
+msgid "Application Number"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:46
+msgid "First Choice"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:48
+msgid "Application State"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:4
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:23
+msgid "Year"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:7
+msgid "Application Period"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:197 waeup/sirp/students/browser.py:713
+#: waeup/sirp/university/interfaces.py:38
+#: waeup/sirp/university/interfaces.py:66
+#: waeup/sirp/university/interfaces.py:86
+#: waeup/sirp/university/interfaces.py:117
+#: waeup/sirp/university/interfaces.py:139
+#: waeup/sirp/university/interfaces.py:179
+#: waeup/sirp/university/interfaces.py:192
+#: waeup/sirp/university/interfaces.py:241
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:39
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:68
+#: waeup/sirp/browser/templates/facultypage.pt:8
+#: waeup/sirp/browser/templates/departmentpage.pt:15
+#: waeup/sirp/browser/templates/departmentpage.pt:39
+#: waeup/sirp/browser/templates/searchpage.pt:13
+#: waeup/sirp/browser/templates/facultymanagepage.pt:41
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:36
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:13
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:34
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:5
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:25
+msgid "Code"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:204 waeup/sirp/students/browser.py:714
+#: waeup/sirp/university/interfaces.py:199
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:41
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:70
+#: waeup/sirp/browser/templates/facultypage.pt:9
+#: waeup/sirp/browser/templates/departmentpage.pt:16
+#: waeup/sirp/browser/templates/departmentpage.pt:40
+#: waeup/sirp/browser/templates/searchpage.pt:14
+#: waeup/sirp/browser/templates/certificatepage.pt:30
+#: waeup/sirp/browser/templates/facultymanagepage.pt:43
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:32
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:37
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:35
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:6
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:27
+msgid "Title"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:211
+#: waeup/sirp/applicants/interfaces.py:324
+msgid "Application Target"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:219
+msgid "Entry Level"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:226
+#: waeup/sirp/applicants/interfaces.py:332
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:234
+#: waeup/sirp/applicants/interfaces.py:340
+msgid "Applicants Container Type"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:250
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:257
+msgid "Human readable description in reST format"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:273
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:279
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:285
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:368
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:373
+msgid "JAMB Registration Number"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:385
+#: waeup/sirp/students/interfaces.py:154
+msgid "First Name"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:389
+#: waeup/sirp/students/interfaces.py:159
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:393
+#: waeup/sirp/students/interfaces.py:164
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:397
+#: waeup/sirp/students/interfaces.py:213
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:402
+msgid "State/LGA"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:407
+#: waeup/sirp/students/interfaces.py:169
+msgid "Sex"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:413
+#: waeup/sirp/students/interfaces.py:490
+msgid "Email Address"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:418
+#: waeup/sirp/students/interfaces.py:203
+msgid "Phone"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:423
+#: waeup/sirp/applicants/interfaces.py:503
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:428
+#: waeup/sirp/applicants/interfaces.py:508
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:437
+#: waeup/sirp/applicants/interfaces.py:513
+msgid "Screening Score"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:441
+#: waeup/sirp/applicants/interfaces.py:518
+msgid "Screening Venue"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:445
+#: waeup/sirp/applicants/interfaces.py:523
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:451
+#: waeup/sirp/applicants/interfaces.py:530
+msgid "Notice"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:473
+#: waeup/sirp/students/interfaces.py:149
+#: waeup/sirp/browser/templates/showstudentspage.pt:4
+#: waeup/sirp/students/browser_templates/containerpage.pt:29
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:35
+msgid "Student Id"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:478
+msgid "Form locked"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:557
+#: waeup/sirp/students/interfaces.py:469
+msgid "Payment Session"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:110
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:23
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:121
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:15
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:182
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:26
+msgid "Admitted Course of Study:"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/pdf.py:200
+msgid ""
+"<font size=10>Proceed to the login page of the portal and enter your new "
+"credentials: user name= ${a}, password = ${b}.</font>"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:206
+msgid "<font size=10>Change your password when you have  logged in.</font>"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:57
+msgid "Application Record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:103
+msgid "Register for application"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:111
+msgid "View application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:119
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:127
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:143
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:151 waeup/sirp/applicants/workflow.py:99
+#: waeup/sirp/students/browser.py:258
+msgid "Create student record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:168 waeup/sirp/students/viewlets.py:300
+msgid "Download payment receipt"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:183 waeup/sirp/students/viewlets.py:315
+msgid "Request callback"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:57
+msgid "Application"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:73 waeup/sirp/students/viewlets.py:512
+msgid "My Data"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:100 waeup/sirp/students/workflow.py:25
+msgid "Student record created"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:106 waeup/sirp/applicants/workflow.py:113
+#: waeup/sirp/applicants/workflow.py:120 waeup/sirp/applicants/workflow.py:127
+msgid "Reset application to started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:107 waeup/sirp/applicants/workflow.py:114
+#: waeup/sirp/applicants/workflow.py:121 waeup/sirp/applicants/workflow.py:128
+#: waeup/sirp/applicants/workflow.py:134
+msgid "Application reset"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:133
+msgid "Reset application to paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:38
+msgid "started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:39
+msgid "paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:40
+msgid "submitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:41 waeup/sirp/interfaces.py:71
+msgid "admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:42
+msgid "not admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:43 waeup/sirp/interfaces.py:70
+msgid "created"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:49
+msgid "Initialize application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:52
+msgid "Application initialized"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:57
+msgid "Start application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:58
+msgid "Application started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:64
+msgid "Pay acceptance fee"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:65
+msgid "Fee paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:71
+msgid "Submit application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:72
+msgid "Application submitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:78
+msgid "Admit applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:79
+msgid "Applicant admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:85 waeup/sirp/applicants/workflow.py:92
+msgid "Refuse application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:86 waeup/sirp/applicants/workflow.py:93
+msgid "Application refused"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:104
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:116 waeup/sirp/browser/viewlets.py:615
+msgid "Academics"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:123 waeup/sirp/browser/pages.py:309
+msgid "Administration"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:130 waeup/sirp/browser/viewlets.py:360
+#: waeup/sirp/browser/viewlets.py:666
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:148 waeup/sirp/browser/pages.py:386
+#: waeup/sirp/browser/viewlets.py:375 waeup/sirp/browser/viewlets.py:676
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:155 waeup/sirp/browser/pages.py:748
+#: waeup/sirp/browser/pages.py:1140 waeup/sirp/browser/viewlets.py:393
+#: waeup/sirp/browser/viewlets.py:685
+msgid "Data Center"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:90
+msgid "Home"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1016
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:103
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1063
+msgid "View processing log"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1064
+msgid "Back to data center"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1108
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1110
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1118
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1120
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1121
+msgid "Show"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:113 waeup/sirp/hostels/browser.py:256
+#: waeup/sirp/hostels/browser.py:287 waeup/sirp/students/browser.py:230
+msgid "No item selected."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1143
+msgid "Back"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1151
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1179
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1199
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1202
+msgid "Given storage path cannot be used."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1203
+msgid "Error: ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1206
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1265
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1293
+#: waeup/sirp/browser/pages.py:1310 waeup/sirp/browser/pages.py:1314
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1278
+msgid "Faculties"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1282 waeup/sirp/browser/viewlets.py:441
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1321
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1342 waeup/sirp/browser/pages.py:1352
+msgid "Departments"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1405
+#: waeup/sirp/browser/pages.py:1424 waeup/sirp/browser/pages.py:1428
+msgid "Add department"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1362 waeup/sirp/browser/viewlets.py:457
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1435
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1438
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1455
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1485 waeup/sirp/browser/pages.py:1504
+#: waeup/sirp/students/browser.py:96 waeup/sirp/students/viewlets.py:472
+msgid "Students"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:152 waeup/sirp/browser/viewlets.py:322
+#: waeup/sirp/browser/templates/staffsitelayout.pt:28
+#: waeup/sirp/browser/templates/studentsitelayout.pt:33
+msgid "Login"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1579
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1591
+#: waeup/sirp/browser/pages.py:1620 waeup/sirp/browser/pages.py:1624
+msgid "Add course"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1522 waeup/sirp/browser/pages.py:1585
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1522 waeup/sirp/browser/pages.py:1596
+#: waeup/sirp/browser/pages.py:1657 waeup/sirp/browser/pages.py:1661
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1531 waeup/sirp/browser/viewlets.py:464
+msgid "Manage department"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:160
+msgid "You logged in."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1636
+msgid "A course with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1642
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1673
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1679
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1711 waeup/sirp/browser/viewlets.py:481
+msgid "Edit course"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1761
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1768 waeup/sirp/browser/pages.py:1799
+#: waeup/sirp/browser/pages.py:1840 waeup/sirp/browser/pages.py:1842
+msgid "Add course referrer"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1769 waeup/sirp/browser/pages.py:1794
+msgid "Remove selected course referrers"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1775 waeup/sirp/browser/viewlets.py:488
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:178
+msgid "You entered wrong credentials."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1847
+msgid "The chosen course referrer is already part of this certificate."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1851
+msgid "Course referrer ${a}_${b} added."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1888 waeup/sirp/browser/viewlets.py:505
+msgid "Edit course referrer"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1891
+msgid "Save and return"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:192
+msgid "You have been logged out. Thanks for using WAeUP SIRP!"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:228
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:234 waeup/sirp/browser/pages.py:519
+msgid "Send message now"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:253 waeup/sirp/browser/pages.py:272
+#: waeup/sirp/browser/pages.py:536 waeup/sirp/students/browser.py:320
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:263
+msgid "Send now"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:274
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:299
+msgid "<h1>Welcome to WAeUP.SIRP</h1>"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:387 waeup/sirp/hostels/browser.py:195
+#: waeup/sirp/students/viewlets.py:163 waeup/sirp/students/viewlets.py:171
+#: waeup/sirp/students/viewlets.py:226 waeup/sirp/students/viewlets.py:243
+#: waeup/sirp/students/viewlets.py:283
+msgid "Manage"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:388 waeup/sirp/browser/viewlets.py:180
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:395
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:426 waeup/sirp/browser/pages.py:428
+#: waeup/sirp/browser/viewlets.py:515
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:450
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:469
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:500
+msgid "User settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:517 waeup/sirp/students/browser.py:301
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:546 waeup/sirp/browser/viewlets.py:400
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:559 waeup/sirp/browser/viewlets.py:415
+msgid "My Roles"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:590
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:617
+msgid "View portal configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:630
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:631 waeup/sirp/browser/pages.py:680
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:633 waeup/sirp/browser/pages.py:661
+#: waeup/sirp/browser/pages.py:692
+msgid "Add session configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:658 waeup/sirp/browser/pages.py:731
+msgid "Settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:672
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:683
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:696
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:703
+msgid "The session chosen already exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:725
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:755
+msgid "Upload file"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:757
+msgid "Upload"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:803 waeup/sirp/browser/pages.py:833
+#: waeup/sirp/browser/pages.py:953 waeup/sirp/browser/pages.py:1061
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:817 waeup/sirp/browser/pages.py:918
+#: waeup/sirp/browser/pages.py:1037
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:83
+msgid "No local role selected."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:836
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:837
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:897
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:905
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:930
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:956 waeup/sirp/browser/pages.py:1182
+msgid "Reset"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:957
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:958
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:959
+msgid "Perform import"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:10
+msgid "Course Code:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:14
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:7
+msgid "Course Title:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:18
+msgid "Provided by:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:26
+msgid "Level:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:30
+msgid "Is mandatory course (not elective):"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:6
+msgid "Code:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:22
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:6
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:26
+msgid "Course Referrers"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:28
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:31
+msgid "Semester"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:29
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:31
+msgid "Referrer"
+msgstr ""
+
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr ""
+
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:8
+msgid "Session Configurations"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:14
+msgid "Datasets"
+msgstr ""
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:43
+msgid "Mode:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:12
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:6
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:4
+msgid "File:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:18
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:9
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:32
+msgid "Processor:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:24
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:13
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:44
+msgid "change to:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:12
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:16
+msgid "Date"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:14
+#: waeup/sirp/browser/templates/datacenterpage.pt:15
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:12
+msgid "Overwrite contents in new location?"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:3
+#: waeup/sirp/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:7
+msgid "Copy existing data to new storage?"
+msgstr ""
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterpage.pt:16
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:10
+#: waeup/sirp/browser/templates/departmentpage.pt:3
+msgid "Courses"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:101
+#: waeup/sirp/browser/templates/facultymanagepage.pt:75
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:92
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:93
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:60
+msgid "Local Role"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:13
+#: waeup/sirp/browser/templates/departmentpage.pt:6
+msgid "Certificates"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:16
+#: waeup/sirp/browser/templates/userscontainerpage.pt:7
+#: waeup/sirp/browser/templates/facultymanagepage.pt:16
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:7
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:17
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:11
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:6
+#: waeup/sirp/browser/templates/facultymanagepage.pt:6
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:5
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:7
+msgid "Settings"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:97
+#: waeup/sirp/browser/templates/userscontainerpage.pt:4
+#: waeup/sirp/browser/templates/facultymanagepage.pt:71
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:88
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:89
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:56
+msgid "User Id"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:99
+#: waeup/sirp/browser/templates/userscontainerpage.pt:5
+#: waeup/sirp/browser/templates/facultymanagepage.pt:73
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:31
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:90
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:91
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:58
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:10
+#: waeup/sirp/hostels/browser_templates/containermanagepage.pt:11
+msgid "Name"
+msgstr ""
+
+#: waeup/sirp/browser/templates/facultypage.pt:1
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:1
+msgid "There no subobjects registered yet."
+msgstr ""
+
+#: waeup/sirp/browser/templates/loginpage.pt:13
+#: waeup/sirp/browser/templates/usereditformpage.pt:8
+#: waeup/sirp/students/browser_templates/basepage.pt:14
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:17
+#: waeup/sirp/students/browser_templates/change_password.pt:10
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:31
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:34
+msgid "Password:"
+msgstr ""
+
+#: waeup/sirp/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/sirp/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 Access Code (PWD) and inititialize your student account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/sirp/browser/templates/loginpage.pt:39
+msgid "login_trouble2"
+msgstr ""
+
+#. Default: "Or simply forgot your student id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/sirp/browser/templates/loginpage.pt:44
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/sirp/browser/templates/loginpage.pt:5
+msgid "User Name or Id:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/sirp/browser/templates/searchpage.pt:15
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:36
+msgid "Type"
+msgstr ""
+
+#: waeup/sirp/browser/templates/searchpage.pt:9
+#: waeup/sirp/students/browser_templates/containerpage.pt:21
+msgid "Search Results"
+msgstr ""
+
+#: waeup/sirp/browser/templates/showstudentspage.pt:5
+msgid "Fullname"
+msgstr ""
+
+#: waeup/sirp/browser/templates/showstudentspage.pt:6
+msgid "Status"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:101
+#: waeup/sirp/browser/templates/studentsitelayout.pt:86
+msgid "Form Status:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:113
+#: waeup/sirp/browser/templates/studentsitelayout.pt:99
+msgid "Back to top"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:114
+#: waeup/sirp/browser/templates/studentsitelayout.pt:100
+msgid ""
+"Student Information and Registration Portal<br /> Copyright &copy; WAeUP "
+"Group, 2008-2012"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:34
+#: waeup/sirp/browser/templates/studentsitelayout.pt:39
+msgid "Contact"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:40
+#: waeup/sirp/browser/templates/studentsitelayout.pt:45
+msgid "Logout"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:60
+msgid "Student Id:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:64
+msgid "State:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/usereditformpage.pt:14
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:23
+#: waeup/sirp/students/browser_templates/change_password.pt:16
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:37
+msgid "Retype password:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:170
+msgid "Add"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:383 waeup/sirp/students/viewlets.py:155
+msgid "Send email"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:434
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:448
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:473 waeup/sirp/browser/viewlets.py:497
+msgid "Show students"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:528 waeup/sirp/browser/viewlets.py:536
+msgid "Show batch logs"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:548
+msgid "Batch processing"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:557
+msgid "Upload CSV file"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:630
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:91 waeup/sirp/browser/viewlets.py:160
+#: waeup/sirp/students/viewlets.py:218 waeup/sirp/students/viewlets.py:403
+msgid "Edit"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:105
+msgid "Accommodation Section"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:113 waeup/sirp/hostels/browser.py:123
+msgid "Manage accommodation section"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:145 waeup/sirp/hostels/browser.py:158
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:161
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:170
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:172
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:207
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:210 waeup/sirp/hostels/browser.py:236
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:211 waeup/sirp/hostels/browser.py:250
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:212 waeup/sirp/hostels/browser.py:281
+msgid "Release selected beds"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:241
+msgid ""
+"${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:275
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:299
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:304
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:316
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:68 waeup/sirp/hostels/browser.py:78
+msgid "Hostels"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:95
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:9
+#: waeup/sirp/hostels/browser_templates/containermanagepage.pt:9
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:35
+msgid "Id"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:37
+msgid "Number"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:56
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:6
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:9
+msgid "Beds"
+msgstr ""
+
+#: waeup/sirp/hostels/hostel.py:180
+msgid "unreserved"
+msgstr ""
+
+#: waeup/sirp/hostels/hostel.py:187
+msgid "reserved"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:105
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:112
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:119
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:126
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:137
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:146
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:169
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:175 waeup/sirp/students/interfaces.py:427
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:181
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:186 waeup/sirp/hostels/interfaces.py:199
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:44
+msgid "Hostel Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:49
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:55
+msgid "Hostel Name"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:61
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:67
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:73
+msgid "Reserved Beds"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:84
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:91
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:98
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:57
+msgid "Bed A"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:58
+msgid "Bed B"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:59
+msgid "Bed C"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:60
+msgid "Bed D"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:61
+msgid "Bed E"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:62
+msgid "Bed F"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:63
+msgid "Bed G"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:64
+msgid "Bed H"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:65
+msgid "Bed I"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:69
+msgid "Block A"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:70
+msgid "Block B"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:71
+msgid "Block C"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:72
+msgid "Block D"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:73
+msgid "Block E"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:74
+msgid "Block F"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:75
+msgid "Block G"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:76
+msgid "Block H"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:77
+msgid "Block I"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:78
+msgid "Block K"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:79
+msgid "Block L"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:80
+msgid "Block M"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:81
+msgid "Block N"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:82
+msgid "Block O"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:83
+msgid "Block P"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:87
+msgid "Regular Hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:88
+msgid "Blocked Hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:89
+msgid "Postgraduate Hostel"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:72
+msgid "clearance started"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:73
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:74
+msgid "cleared"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:75
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:76
+msgid "returning"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:77
+msgid "courses registered"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:78
+msgid "courses validated"
+msgstr ""
+
+#: waeup/sirp/objecthistory.py:73
+msgid "by"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:106
+msgid "Surcharge 1"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:113
+msgid "Surcharge 2"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:120
+msgid "Surcharge 3"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:133
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:140
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:147
+msgid "Response Description"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:154
+msgid "Response Payment Reference"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:161
+msgid "Response Code"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:168
+msgid "Response Card Number"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:24
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:25
+msgid "Paid"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:26
+msgid "Failed"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:30
+msgid "School Fee"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:31
+msgid "Clearance"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:32
+msgid "Bed Allocation"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:33
+msgid "Hostel Maintenance"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:34
+msgid "Transfer"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:35
+msgid "Gown"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:36
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:51
+msgid "Payment Category"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:58
+msgid "Payment Item"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:64
+msgid "Payment State"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:71
+msgid "Ticket Creation Date"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:76
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:10
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:84
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "Payment Date"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:82
+msgid "Amount Authorized"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:94
+msgid "Payment Access Code"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1003 waeup/sirp/students/browser.py:1225
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1072
+msgid "This ticket has already been paid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1085 waeup/sirp/students/browser.py:1093
+#: waeup/sirp/students/browser.py:1101
+msgid "Valid callback received. ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1155
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1167
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:117 waeup/sirp/students/viewlets.py:114
+#: waeup/sirp/students/viewlets.py:534
+msgid "Study Course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1174
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1205
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1207
+msgid "Create bed ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1215
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1219
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:123 waeup/sirp/students/viewlets.py:120
+#: waeup/sirp/students/viewlets.py:535
+msgid "Payments"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1230
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1241 waeup/sirp/students/browser.py:1537
+#: waeup/sirp/students/browser.py:1650
+msgid "Activation code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1261 waeup/sirp/students/browser.py:1389
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1266 waeup/sirp/students/browser.py:1542
+#: waeup/sirp/students/browser.py:1655
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1269 waeup/sirp/students/browser.py:1545
+#: waeup/sirp/students/browser.py:1658 waeup/sirp/students/browser.py:1917
+msgid "invalidated"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1274 waeup/sirp/students/browser.py:1549
+#: waeup/sirp/students/browser.py:1663
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1285 waeup/sirp/students/browser.py:1403
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1294
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1312
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1328
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1334
+msgid "Bed Allocation: "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1361
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1367
+msgid "Student can't be relocated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:138 waeup/sirp/students/viewlets.py:126
+msgid "Accommodation"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1410
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1429
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1441 waeup/sirp/students/viewlets.py:356
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1456 waeup/sirp/students/viewlets.py:365
+msgid "Change password"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:147
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1470
+msgid "Password changed."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1482
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1497 waeup/sirp/students/viewlets.py:389
+#: waeup/sirp/students/workflow.py:44
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1501
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1518 waeup/sirp/students/browser.py:1639
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1522
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1526
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1553
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1565
+msgid "Edit clearance data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1577 waeup/sirp/students/browser.py:1591
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1585
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1600 waeup/sirp/students/workflow.py:58
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1601
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1604
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1613
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1622
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1631 waeup/sirp/students/viewlets.py:418
+msgid "Start course registration"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1635
+msgid "Start course registration now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1671
+msgid "Course registration has been started."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1690
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1699
+msgid "Create course list now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:170
+msgid "Student Section"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1738
+msgid "Add and remove course tickets of study level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1775
+msgid "Register course list"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1779
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1825
+msgid "Change my password"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1835
+msgid "Get new login credentials"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1849
+msgid "No student record found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1857
+msgid "You have successfully changed your password for the"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:186 waeup/sirp/students/browser.py:222
+msgid "Empty search string"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1862
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1873
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1876
+msgid "Set"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1891
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1899
+msgid "Access code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1902
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1908
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1914
+msgid "Access code has already been used."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1923
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:204 waeup/sirp/students/viewlets.py:139
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:228 waeup/sirp/students/browser.py:1888
+msgid "No student found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:255 waeup/sirp/students/viewlets.py:146
+msgid "Add student"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:263
+msgid "Student record created."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:279
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:322 waeup/sirp/students/browser.py:1865
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:333
+msgid "Manage base data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:402
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:418 waeup/sirp/students/viewlets.py:104
+#: waeup/sirp/students/viewlets.py:532
+#: waeup/sirp/students/browser_templates/clearanceeditpage.pt:6
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:424
+msgid "Clearance Slip of "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:444
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:473
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:475 waeup/sirp/students/browser.py:499
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:492
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:496
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:522
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:532
+msgid "Manage personal data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:552
+msgid "${a}: Study Course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:582
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:585 waeup/sirp/students/browser.py:626
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:586 waeup/sirp/students/browser.py:612
+msgid "Add study level"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:620
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:622 waeup/sirp/students/browser.py:1708
+msgid "This level exists."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:632
+msgid "No study level selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:668
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:691
+msgid "Level Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:697
+msgid "Course List"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:706
+msgid "Course Registration Slip  "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:712
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:35
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:12
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:33
+msgid "Sem."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:715
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:38
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:36
+msgid "Dept."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:716
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:16
+msgid "Faculty"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:717
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:40
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:38
+msgid "Cred."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:718
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:39
+msgid "Mand."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:719 waeup/sirp/students/interfaces.py:370
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:40
+msgid "Score"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:747 waeup/sirp/students/browser.py:776
+#: waeup/sirp/students/browser.py:861 waeup/sirp/students/browser.py:866
+#: waeup/sirp/students/browser.py:1748 waeup/sirp/students/browser.py:1798
+msgid "Add course ticket"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:768
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:786 waeup/sirp/students/browser.py:1758
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:810 waeup/sirp/students/browser.py:832
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:814
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:816 waeup/sirp/students/browser.py:844
+msgid "Student is in the wrong state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:837
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:841
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:881 waeup/sirp/students/browser.py:1811
+msgid "The ticket exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:883 waeup/sirp/students/browser.py:1813
+msgid "Successfully added ${a}."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:904
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:920
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:947
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:992
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:995
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:11
+msgid "Bed Category"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:29
+msgid "Registration Transition:"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/sirp/students/browser_templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/clearanceeditpage.pt:8
+msgid "Scans"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:28
+msgid "Select all"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:10
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:10
+msgid "with registration number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:11
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:11
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:12
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:12
+msgid "in session"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:13
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:13
+msgid "in department"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:14
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:14
+msgid "in study mode"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:30
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:36
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:31
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:37
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:32
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:38
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:14
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:25
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:87
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:54
+msgid "State"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:33
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:39
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:44
+msgid "Full Name"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:5
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:5
+msgid "for students"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:8
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:8
+msgid "with id"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:9
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:9
+msgid "with name"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:31
+msgid "Semester:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:39
+msgid "Credits:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:47
+msgid "Passmark:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/imageupload.pt:20
+#: waeup/sirp/students/browser_templates/fileupload.pt:27
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:25
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:11
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:85
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:52
+msgid "Category"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:12
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:86
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:53
+msgid "Item"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:8
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:82
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:49
+msgid "Payment Id"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:83
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:50
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/setpassword.pt:14
+msgid "Access Code:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/setpassword.pt:32
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/sirp/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/sirp/students/browser_templates/setpassword.pt:5
+msgid "Registration Number:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:6
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:9
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:52
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:30
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:57
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:35
+msgid "Level Code"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:58
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:36
+msgid "Level Title"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:71
+msgid "There are no levels registered yet."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveladdpage.pt:4
+msgid "Text to explain course registration procedure."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:15
+msgid "Department"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:6
+msgid "Course Tickets (Total Credits: ${DYNAMIC_CONTENT})"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:39
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:37
+msgid "Fact."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:43
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:19
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:41
+msgid "CO"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:44
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:42
+msgid "Auto"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:6
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:9
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:30
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:28
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:18
+msgid "Total Credits:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:64
+msgid "The're no tickets added yet."
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:176 waeup/sirp/students/interfaces.py:255
+#: waeup/sirp/students/interfaces.py:483
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:183 waeup/sirp/students/interfaces.py:265
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:191
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:198
+msgid "Email"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:218
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:223
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:231
+msgid "Nationality"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:241
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:275
+#: waeup/sirp/browser/templates/showstudentspage.pt:7
+msgid "Certificate"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:282
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:290
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:299
+#: waeup/sirp/browser/templates/showstudentspage.pt:8
+msgid "Current Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:307
+#: waeup/sirp/browser/templates/showstudentspage.pt:9
+msgid "Current Level"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:315
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:322
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:337 waeup/sirp/students/interfaces.py:434
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:8
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:13
+msgid "Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:344
+msgid "Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:363
+#: waeup/sirp/browser/templates/certificatepage.pt:30
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:33
+msgid "Mandatory"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:377
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:384
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:403
+#: waeup/sirp/university/interfaces.py:299
+#: waeup/sirp/browser/templates/certificatepage.pt:29
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:32
+msgid "Course"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:420
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:10
+msgid "Bed Coordinates"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:442
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:448
+msgid "Booking Activation Code"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/utils.py:163
+msgid "<font size=10>${a} Page ${b} of ${c}</font>"
+msgstr ""
+
+#: waeup/sirp/students/utils.py:276 waeup/sirp/students/viewlets.py:71
+#: waeup/sirp/students/viewlets.py:99
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:6
+msgid "Base Data"
+msgstr ""
+
+#: waeup/sirp/students/utils.py:307
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:109 waeup/sirp/students/viewlets.py:533
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:131 waeup/sirp/students/viewlets.py:537
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:27
+msgid "History"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:179 waeup/sirp/students/workflow.py:72
+msgid "Clear student"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:194
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:210
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:235
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:251 waeup/sirp/students/workflow.py:128
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:267
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:330
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:339
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:348
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:374
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:432
+msgid "Add course list"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:450
+msgid "Add and remove courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:536
+msgid "Accommodation Data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:548
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:561
+msgid "File name has no extension."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:564
+msgid "File name contains more than one dot."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:571
+msgid "${a} file extension expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:577
+msgid "Only the following extension are allowed: ${a}"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:583
+msgid "Uploaded file is too big."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:590
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:608
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:10
+#: waeup/sirp/browser/templates/datacenterpage.pt:14
+#: waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt:11
+msgid "File"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:609
+msgid "Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:632
+msgid "Upload new file"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:633
+msgid "Delete attachment"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:663
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:677
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:8
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:688
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:703 waeup/sirp/students/viewlets.py:714
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:704 waeup/sirp/students/viewlets.py:715
+msgid "Birth Certificate Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:724 waeup/sirp/students/viewlets.py:735
+msgid "Acceptance Letter"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:725 waeup/sirp/students/viewlets.py:736
+msgid "Acceptance Letter Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:85
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:135
+msgid "Male"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:137
+msgid "Female"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:77
+msgid "Error: level id ${value} out of range"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:88
+msgid "${title} 2nd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:91
+msgid "${title} 3rd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:94
+msgid "${title} 1st spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:96
+msgid "${title} on 1st probation"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:98
+msgid "${title} on 2nd probation"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:107
+msgid "Reset to returning"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:108
+msgid "Student record reset to 'returning'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:114
+msgid "Register courses"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:115
+msgid "Courses registered"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:121 waeup/sirp/students/workflow.py:135
+msgid "Reset to paid"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:122 waeup/sirp/students/workflow.py:136
+msgid "Student record reset to 'paid'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:129
+msgid "Courses validated"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:142
+msgid "Return"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:143
+msgid "Returned"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:149
+msgid "Reset to validated"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:150
+msgid "Student record reset to 'validated'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:22
+msgid "Create student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:30
+msgid "Admit student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:31
+msgid "Student admitted"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:37
+msgid "Reset student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:38
+msgid "Student record reset"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:45
+msgid "Clearance started"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:51
+msgid "Reset to admitted"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:52
+msgid "Student record reset to 'admitted'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:59
+msgid "Clearance requested"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:65 waeup/sirp/students/workflow.py:79
+msgid "Reset to clearance"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:66 waeup/sirp/students/workflow.py:80
+msgid "Student record reset to 'clearance'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:73
+msgid "Cleared"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:86 waeup/sirp/students/workflow.py:100
+msgid "Pay school fee"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:87 waeup/sirp/students/workflow.py:101
+msgid "School fee paid"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:93
+msgid "Reset to cleared"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:94
+msgid "Student record reset to 'cleared'"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:146
+msgid "Title of course"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:152
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:17
+msgid "Credits"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:158
+msgid "Passmark"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:164
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:180
+msgid "Enter unique course code which will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:205
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:212
+msgid "Start Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:219
+msgid "End Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:226
+msgid "Application Category"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:243
+msgid "Enter unique certificate code which will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:264
+msgid "Course Referrer"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:270
+#: waeup/sirp/browser/templates/certificatepage.pt:28
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:30
+msgid "Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:277
+msgid "Is mandatory course (not elective)"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:45
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:46
+#: waeup/sirp/university/interfaces.py:94
+#: waeup/sirp/university/interfaces.py:147
+#: waeup/sirp/university/interfaces.py:200
+msgid "Unnamed"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:51
+#: waeup/sirp/university/interfaces.py:99
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:67
+#: waeup/sirp/university/interfaces.py:118
+msgid "This code will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:93
+msgid "Name of department"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:28
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:29
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:30
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:31
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:32
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:33
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:34
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:35
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:36
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/sirp/utils/utils.py:119
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/sirp/utils/utils.py:164
+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"
+"Login page: ${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 ""
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/ig/LC_MESSAGES/waeup.sirp.po
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/ig/LC_MESSAGES/waeup.sirp.po	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/ig/LC_MESSAGES/waeup.sirp.po	(revision 7760)
@@ -0,0 +1,3570 @@
+# #############################################################################
+#
+# 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.SIRP\n"
+"POT-Creation-Date: Fri Mar  2 12:58:47 2012\n"
+"PO-Revision-Date: 2012-02-29 12:22+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/sirp/accesscodes/browser.py:100 waeup/sirp/accesscodes/browser.py:113
+#: waeup/sirp/applicants/browser.py:80 waeup/sirp/applicants/browser.py:130
+#: waeup/sirp/applicants/browser.py:182 waeup/sirp/applicants/browser.py:254
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:338
+#: waeup/sirp/browser/pages.py:503 waeup/sirp/browser/pages.py:708
+#: waeup/sirp/browser/pages.py:735 waeup/sirp/browser/pages.py:758
+#: waeup/sirp/browser/pages.py:805 waeup/sirp/browser/pages.py:835
+#: waeup/sirp/browser/pages.py:955 waeup/sirp/browser/pages.py:1183
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1298
+#: waeup/sirp/browser/pages.py:1325 waeup/sirp/browser/pages.py:1353
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1400
+#: waeup/sirp/browser/pages.py:1441 waeup/sirp/browser/pages.py:1520
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1523
+#: waeup/sirp/browser/pages.py:1601 waeup/sirp/browser/pages.py:1646
+#: waeup/sirp/browser/pages.py:1684 waeup/sirp/browser/pages.py:1728
+#: waeup/sirp/browser/pages.py:1767 waeup/sirp/browser/pages.py:1769
+#: waeup/sirp/browser/pages.py:1804 waeup/sirp/browser/pages.py:1855
+#: waeup/sirp/browser/pages.py:1907 waeup/sirp/students/browser.py:584
+#: waeup/sirp/students/browser.py:585 waeup/sirp/students/browser.py:746
+#: waeup/sirp/students/browser.py:748 waeup/sirp/students/browser.py:888
+msgid "Cancel"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:102
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:112
+msgid "Reimport"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:115
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:120
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:126
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:136
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:139
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:152
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:153 waeup/sirp/browser/pages.py:592
+#: waeup/sirp/browser/pages.py:1142 waeup/sirp/browser/viewlets.py:190
+#: waeup/sirp/students/browser.py:171 waeup/sirp/students/browser.py:205
+msgid "Search"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:154
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:155
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:180 waeup/sirp/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:182
+msgid "${a} disabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:186
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:190
+msgid "re-enabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:192
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:196
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:235
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:244
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:252
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:261
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:43
+msgid "Archive"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:44
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:46 waeup/sirp/accesscodes/browser.py:206
+#: waeup/sirp/accesscodes/browser.py:226
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:53
+msgid "No batch selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:61
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:68
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:77
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:83
+msgid "Create batch"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:91
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:93
+msgid "Data written to ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:10
+msgid "Entries/(invalidated)"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:11
+msgid "Cost"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:12
+msgid "Date of Creation"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:13
+msgid "Creator"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:2
+msgid "The following batches are available:"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:39
+msgid "No batches yet"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:9
+msgid "Prefix"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt:23
+msgid "No import batches available"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:23
+msgid "Serial"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:24
+msgid "AC"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:26
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:38
+msgid "Owner"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:5
+msgid "for access codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:7
+msgid "with PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:8
+msgid "with serial"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:9
+msgid "with message term"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:29
+msgid "Serial number inside batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:32
+msgid "Prefix inside batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:35
+msgid "Batch number"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:38
+msgid "Random part of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:41
+#: waeup/sirp/accesscodes/interfaces.py:80
+msgid "Cost of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:45
+msgid "Workflow state"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:48
+msgid "Complete title of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:51
+msgid "Purchaser"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:54
+msgid "The history of access code as lines"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:63
+msgid "Creation date"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:66
+msgid "Batch creator"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:69
+msgid "Batch prefix"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:72
+msgid "Batch number (1-3 digits)"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:76
+msgid "Number of access codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:84
+msgid "Number of disabled access codes inside the batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:89
+msgid "Number of used access codes inside the batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:102
+msgid "Disable used PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:109
+msgid "Reenable disabled PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:37 waeup/sirp/applicants/workflow.py:37
+msgid "initialized"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:38
+msgid "used"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:81
+msgid "Initialize PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:88
+msgid "Use PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:95
+msgid "Disable unused PIN"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:117 waeup/sirp/applicants/browser.py:325
+#: waeup/sirp/applicants/browser.py:745
+msgid "Could not delete:"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:120 waeup/sirp/applicants/browser.py:328
+#: waeup/sirp/applicants/browser.py:748 waeup/sirp/browser/pages.py:134
+#: waeup/sirp/students/browser.py:243 waeup/sirp/students/browser.py:642
+#: waeup/sirp/students/browser.py:796 waeup/sirp/students/browser.py:974
+#: waeup/sirp/students/browser.py:1184 waeup/sirp/students/browser.py:1770
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:169
+msgid ""
+"An applicants container for the same application type and entrance year "
+"exists already in the database."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:178
+msgid "Added:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:190 waeup/sirp/applicants/viewlets.py:44
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:12
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:36
+msgid "Applicants"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:254 waeup/sirp/applicants/browser.py:301
+#: waeup/sirp/applicants/browser.py:654 waeup/sirp/applicants/browser.py:688
+#: waeup/sirp/applicants/browser.py:791 waeup/sirp/applicants/browser.py:794
+#: waeup/sirp/applicants/browser.py:838 waeup/sirp/browser/pages.py:476
+#: waeup/sirp/browser/pages.py:631 waeup/sirp/browser/pages.py:654
+#: waeup/sirp/browser/pages.py:728 waeup/sirp/browser/pages.py:1181
+#: waeup/sirp/browser/pages.py:1353 waeup/sirp/browser/pages.py:1394
+#: waeup/sirp/browser/pages.py:1520 waeup/sirp/browser/pages.py:1573
+#: waeup/sirp/browser/pages.py:1716 waeup/sirp/browser/pages.py:1767
+#: waeup/sirp/browser/pages.py:1788 waeup/sirp/hostels/browser.py:209
+#: waeup/sirp/hostels/browser.py:231 waeup/sirp/hostels/browser.py:319
+#: waeup/sirp/students/browser.py:359 waeup/sirp/students/browser.py:458
+#: waeup/sirp/students/browser.py:535 waeup/sirp/students/browser.py:584
+#: waeup/sirp/students/browser.py:600 waeup/sirp/students/browser.py:746
+#: waeup/sirp/students/browser.py:771 waeup/sirp/students/browser.py:1444
+#: waeup/sirp/students/browser.py:1459 waeup/sirp/students/browser.py:1574
+msgid "Save"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:333
+#: waeup/sirp/applicants/browser.py:361
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:265 waeup/sirp/applicants/viewlets.py:95
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:305 waeup/sirp/applicants/browser.py:717
+#: waeup/sirp/browser/pages.py:1397 waeup/sirp/browser/pages.py:1719
+#: waeup/sirp/browser/pages.py:1791 waeup/sirp/browser/pages.py:1903
+#: waeup/sirp/hostels/browser.py:54 waeup/sirp/students/browser.py:80
+#: waeup/sirp/students/browser.py:385
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:314
+msgid "No applicant selected!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:364
+msgid "Create application record"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:369
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:382
+msgid "Applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:397 waeup/sirp/students/browser.py:285
+msgid "set"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:398 waeup/sirp/students/browser.py:286
+msgid "unset"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:403
+msgid "${a} Application Record ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:454
+msgid "Session configuration object is not available."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:461 waeup/sirp/students/browser.py:1023
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:474 waeup/sirp/students/browser.py:1037
+msgid "Payment ticket created."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:501 waeup/sirp/students/browser.py:1056
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:529 waeup/sirp/students/browser.py:1105
+msgid "Valid callback received."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:550
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:556
+msgid "Online Payment Receipt"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:562
+msgid "Ticket not yet paid."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:627
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:632
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:64
+msgid "Application Section"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:654 waeup/sirp/applicants/browser.py:794
+#: waeup/sirp/applicants/browser.py:846
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:655 waeup/sirp/applicants/browser.py:758
+#: waeup/sirp/applicants/browser.py:792 waeup/sirp/applicants/browser.py:827
+#: waeup/sirp/students/browser.py:980
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:655 waeup/sirp/applicants/browser.py:763
+#: waeup/sirp/applicants/browser.py:792 waeup/sirp/applicants/browser.py:795
+#: waeup/sirp/applicants/browser.py:832 waeup/sirp/students/browser.py:748
+#: waeup/sirp/students/browser.py:780 waeup/sirp/students/browser.py:956
+#: waeup/sirp/students/browser.py:1752
+msgid "Remove selected tickets"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:674
+msgid "${a} Application Form ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:685 waeup/sirp/students/browser.py:356
+msgid "No transition"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:732 waeup/sirp/students/browser.py:962
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:76 waeup/sirp/applicants/viewlets.py:88
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:79 waeup/sirp/applicants/browser.py:105
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:308
+#: waeup/sirp/browser/pages.py:634 waeup/sirp/browser/pages.py:675
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1288
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1389
+#: waeup/sirp/hostels/browser.py:131 waeup/sirp/students/browser.py:206
+#: waeup/sirp/students/browser.py:1151 waeup/sirp/students/browser.py:1164
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:79 waeup/sirp/applicants/browser.py:125
+#: waeup/sirp/applicants/browser.py:148 waeup/sirp/applicants/browser.py:160
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:805 waeup/sirp/students/browser.py:88
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:81 waeup/sirp/applicants/browser.py:139
+#: waeup/sirp/applicants/browser.py:256 waeup/sirp/applicants/browser.py:347
+#: waeup/sirp/browser/pages.py:1355 waeup/sirp/browser/pages.py:1414
+#: waeup/sirp/browser/pages.py:1524 waeup/sirp/browser/pages.py:1610
+#: waeup/sirp/browser/pages.py:1770 waeup/sirp/browser/pages.py:1827
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:819
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:82 waeup/sirp/applicants/browser.py:135
+#: waeup/sirp/applicants/browser.py:257 waeup/sirp/applicants/browser.py:343
+#: waeup/sirp/browser/pages.py:1356 waeup/sirp/browser/pages.py:1410
+#: waeup/sirp/browser/pages.py:1525 waeup/sirp/browser/pages.py:1606
+#: waeup/sirp/browser/pages.py:1771 waeup/sirp/browser/pages.py:1823
+msgid "Add local role"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:821
+msgid "Passport picture confirmation box not ticked."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:83
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:858 waeup/sirp/students/browser.py:1619
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:863
+msgid "Form has been submitted."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:898
+msgid "Register for ${a} Application"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:904
+msgid "Application has not yet started."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:909
+msgid "Application has ended."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:918
+msgid "Get login credentials"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:933
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:940
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:949
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:18
+msgid "Photograph"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:43
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:59
+msgid ""
+"I confirm that the Passport Photograph uploaded on this form is a true "
+"picture of me."
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:74
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:43
+msgid "Acceptance Fee Payment Tickets"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:11
+msgid "Thanks for using SIRP!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:3
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/sirp/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/sirp/applicants/browser_templates/applicantregister.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:42
+msgid "Application Number"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:46
+msgid "First Choice"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:48
+msgid "Application State"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:4
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:23
+msgid "Year"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:7
+msgid "Application Period"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:197 waeup/sirp/students/browser.py:713
+#: waeup/sirp/university/interfaces.py:38
+#: waeup/sirp/university/interfaces.py:66
+#: waeup/sirp/university/interfaces.py:86
+#: waeup/sirp/university/interfaces.py:117
+#: waeup/sirp/university/interfaces.py:139
+#: waeup/sirp/university/interfaces.py:179
+#: waeup/sirp/university/interfaces.py:192
+#: waeup/sirp/university/interfaces.py:241
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:39
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:68
+#: waeup/sirp/browser/templates/facultypage.pt:8
+#: waeup/sirp/browser/templates/departmentpage.pt:15
+#: waeup/sirp/browser/templates/departmentpage.pt:39
+#: waeup/sirp/browser/templates/searchpage.pt:13
+#: waeup/sirp/browser/templates/facultymanagepage.pt:41
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:36
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:13
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:34
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:5
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:25
+msgid "Code"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:204 waeup/sirp/students/browser.py:714
+#: waeup/sirp/university/interfaces.py:199
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:41
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:70
+#: waeup/sirp/browser/templates/facultypage.pt:9
+#: waeup/sirp/browser/templates/departmentpage.pt:16
+#: waeup/sirp/browser/templates/departmentpage.pt:40
+#: waeup/sirp/browser/templates/searchpage.pt:14
+#: waeup/sirp/browser/templates/certificatepage.pt:30
+#: waeup/sirp/browser/templates/facultymanagepage.pt:43
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:32
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:37
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:35
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:6
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:27
+msgid "Title"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:211
+#: waeup/sirp/applicants/interfaces.py:324
+msgid "Application Target"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:219
+msgid "Entry Level"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:226
+#: waeup/sirp/applicants/interfaces.py:332
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:234
+#: waeup/sirp/applicants/interfaces.py:340
+msgid "Applicants Container Type"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:250
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:257
+msgid "Human readable description in reST format"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:273
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:279
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:285
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:368
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:373
+msgid "JAMB Registration Number"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:385
+#: waeup/sirp/students/interfaces.py:154
+msgid "First Name"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:389
+#: waeup/sirp/students/interfaces.py:159
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:393
+#: waeup/sirp/students/interfaces.py:164
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:397
+#: waeup/sirp/students/interfaces.py:213
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:402
+msgid "State/LGA"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:407
+#: waeup/sirp/students/interfaces.py:169
+msgid "Sex"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:413
+#: waeup/sirp/students/interfaces.py:490
+msgid "Email Address"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:418
+#: waeup/sirp/students/interfaces.py:203
+msgid "Phone"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:423
+#: waeup/sirp/applicants/interfaces.py:503
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:428
+#: waeup/sirp/applicants/interfaces.py:508
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:437
+#: waeup/sirp/applicants/interfaces.py:513
+msgid "Screening Score"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:441
+#: waeup/sirp/applicants/interfaces.py:518
+msgid "Screening Venue"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:445
+#: waeup/sirp/applicants/interfaces.py:523
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:451
+#: waeup/sirp/applicants/interfaces.py:530
+msgid "Notice"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:473
+#: waeup/sirp/students/interfaces.py:149
+#: waeup/sirp/browser/templates/showstudentspage.pt:4
+#: waeup/sirp/students/browser_templates/containerpage.pt:29
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:35
+msgid "Student Id"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:478
+msgid "Form locked"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:557
+#: waeup/sirp/students/interfaces.py:469
+msgid "Payment Session"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:110
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:23
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:121
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:15
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:182
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:26
+msgid "Admitted Course of Study:"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/pdf.py:200
+msgid ""
+"<font size=10>Proceed to the login page of the portal and enter your new "
+"credentials: user name= ${a}, password = ${b}.</font>"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:206
+msgid "<font size=10>Change your password when you have  logged in.</font>"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:57
+msgid "Application Record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:103
+msgid "Register for application"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:111
+msgid "View application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:119
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:127
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:143
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:151 waeup/sirp/applicants/workflow.py:99
+#: waeup/sirp/students/browser.py:258
+msgid "Create student record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:168 waeup/sirp/students/viewlets.py:300
+msgid "Download payment receipt"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:183 waeup/sirp/students/viewlets.py:315
+msgid "Request callback"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:57
+msgid "Application"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:73 waeup/sirp/students/viewlets.py:512
+msgid "My Data"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:100 waeup/sirp/students/workflow.py:25
+msgid "Student record created"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:106 waeup/sirp/applicants/workflow.py:113
+#: waeup/sirp/applicants/workflow.py:120 waeup/sirp/applicants/workflow.py:127
+msgid "Reset application to started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:107 waeup/sirp/applicants/workflow.py:114
+#: waeup/sirp/applicants/workflow.py:121 waeup/sirp/applicants/workflow.py:128
+#: waeup/sirp/applicants/workflow.py:134
+msgid "Application reset"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:133
+msgid "Reset application to paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:38
+msgid "started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:39
+msgid "paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:40
+msgid "submitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:41 waeup/sirp/interfaces.py:71
+msgid "admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:42
+msgid "not admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:43 waeup/sirp/interfaces.py:70
+msgid "created"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:49
+msgid "Initialize application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:52
+msgid "Application initialized"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:57
+msgid "Start application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:58
+msgid "Application started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:64
+msgid "Pay acceptance fee"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:65
+msgid "Fee paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:71
+msgid "Submit application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:72
+msgid "Application submitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:78
+msgid "Admit applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:79
+msgid "Applicant admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:85 waeup/sirp/applicants/workflow.py:92
+msgid "Refuse application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:86 waeup/sirp/applicants/workflow.py:93
+msgid "Application refused"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:104
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:116 waeup/sirp/browser/viewlets.py:615
+msgid "Academics"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:123 waeup/sirp/browser/pages.py:309
+msgid "Administration"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:130 waeup/sirp/browser/viewlets.py:360
+#: waeup/sirp/browser/viewlets.py:666
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:148 waeup/sirp/browser/pages.py:386
+#: waeup/sirp/browser/viewlets.py:375 waeup/sirp/browser/viewlets.py:676
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:155 waeup/sirp/browser/pages.py:748
+#: waeup/sirp/browser/pages.py:1140 waeup/sirp/browser/viewlets.py:393
+#: waeup/sirp/browser/viewlets.py:685
+msgid "Data Center"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:90
+msgid "Home"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1016
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:103
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1063
+msgid "View processing log"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1064
+msgid "Back to data center"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1108
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1110
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1118
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1120
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1121
+msgid "Show"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:113 waeup/sirp/hostels/browser.py:256
+#: waeup/sirp/hostels/browser.py:287 waeup/sirp/students/browser.py:230
+msgid "No item selected."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1143
+msgid "Back"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1151
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1179
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1199
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1202
+msgid "Given storage path cannot be used."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1203
+msgid "Error: ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1206
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1265
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1293
+#: waeup/sirp/browser/pages.py:1310 waeup/sirp/browser/pages.py:1314
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1278
+msgid "Faculties"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1282 waeup/sirp/browser/viewlets.py:441
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1321
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1342 waeup/sirp/browser/pages.py:1352
+msgid "Departments"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1405
+#: waeup/sirp/browser/pages.py:1424 waeup/sirp/browser/pages.py:1428
+msgid "Add department"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1362 waeup/sirp/browser/viewlets.py:457
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1435
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1438
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1455
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1485 waeup/sirp/browser/pages.py:1504
+#: waeup/sirp/students/browser.py:96 waeup/sirp/students/viewlets.py:472
+msgid "Students"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:152 waeup/sirp/browser/viewlets.py:322
+#: waeup/sirp/browser/templates/staffsitelayout.pt:28
+#: waeup/sirp/browser/templates/studentsitelayout.pt:33
+msgid "Login"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1579
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1591
+#: waeup/sirp/browser/pages.py:1620 waeup/sirp/browser/pages.py:1624
+msgid "Add course"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1522 waeup/sirp/browser/pages.py:1585
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1522 waeup/sirp/browser/pages.py:1596
+#: waeup/sirp/browser/pages.py:1657 waeup/sirp/browser/pages.py:1661
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1531 waeup/sirp/browser/viewlets.py:464
+msgid "Manage department"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:160
+msgid "You logged in."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1636
+msgid "A course with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1642
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1673
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1679
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1711 waeup/sirp/browser/viewlets.py:481
+msgid "Edit course"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1761
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1768 waeup/sirp/browser/pages.py:1799
+#: waeup/sirp/browser/pages.py:1840 waeup/sirp/browser/pages.py:1842
+msgid "Add course referrer"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1769 waeup/sirp/browser/pages.py:1794
+msgid "Remove selected course referrers"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1775 waeup/sirp/browser/viewlets.py:488
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:178
+msgid "You entered wrong credentials."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1847
+msgid "The chosen course referrer is already part of this certificate."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1851
+msgid "Course referrer ${a}_${b} added."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1888 waeup/sirp/browser/viewlets.py:505
+msgid "Edit course referrer"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1891
+msgid "Save and return"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:192
+msgid "You have been logged out. Thanks for using WAeUP SIRP!"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:228
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:234 waeup/sirp/browser/pages.py:519
+msgid "Send message now"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:253 waeup/sirp/browser/pages.py:272
+#: waeup/sirp/browser/pages.py:536 waeup/sirp/students/browser.py:320
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:263
+msgid "Send now"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:274
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:299
+msgid "<h1>Welcome to WAeUP.SIRP</h1>"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:387 waeup/sirp/hostels/browser.py:195
+#: waeup/sirp/students/viewlets.py:163 waeup/sirp/students/viewlets.py:171
+#: waeup/sirp/students/viewlets.py:226 waeup/sirp/students/viewlets.py:243
+#: waeup/sirp/students/viewlets.py:283
+msgid "Manage"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:388 waeup/sirp/browser/viewlets.py:180
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:395
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:426 waeup/sirp/browser/pages.py:428
+#: waeup/sirp/browser/viewlets.py:515
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:450
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:469
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:500
+msgid "User settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:517 waeup/sirp/students/browser.py:301
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:546 waeup/sirp/browser/viewlets.py:400
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:559 waeup/sirp/browser/viewlets.py:415
+msgid "My Roles"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:590
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:617
+msgid "View portal configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:630
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:631 waeup/sirp/browser/pages.py:680
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:633 waeup/sirp/browser/pages.py:661
+#: waeup/sirp/browser/pages.py:692
+msgid "Add session configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:658 waeup/sirp/browser/pages.py:731
+msgid "Settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:672
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:683
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:696
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:703
+msgid "The session chosen already exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:725
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:755
+msgid "Upload file"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:757
+msgid "Upload"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:803 waeup/sirp/browser/pages.py:833
+#: waeup/sirp/browser/pages.py:953 waeup/sirp/browser/pages.py:1061
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:817 waeup/sirp/browser/pages.py:918
+#: waeup/sirp/browser/pages.py:1037
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:83
+msgid "No local role selected."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:836
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:837
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:897
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:905
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:930
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:956 waeup/sirp/browser/pages.py:1182
+msgid "Reset"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:957
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:958
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:959
+msgid "Perform import"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:10
+msgid "Course Code:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:14
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:7
+msgid "Course Title:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:18
+msgid "Provided by:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:26
+msgid "Level:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:30
+msgid "Is mandatory course (not elective):"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:6
+msgid "Code:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:22
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:6
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:26
+msgid "Course Referrers"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:28
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:31
+msgid "Semester"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:29
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:31
+msgid "Referrer"
+msgstr ""
+
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr ""
+
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:8
+msgid "Session Configurations"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:14
+msgid "Datasets"
+msgstr ""
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:43
+msgid "Mode:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:12
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:6
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:4
+msgid "File:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:18
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:9
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:32
+msgid "Processor:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:24
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:13
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:44
+msgid "change to:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:12
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:16
+msgid "Date"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:14
+#: waeup/sirp/browser/templates/datacenterpage.pt:15
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:12
+msgid "Overwrite contents in new location?"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:3
+#: waeup/sirp/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:7
+msgid "Copy existing data to new storage?"
+msgstr ""
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterpage.pt:16
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:10
+#: waeup/sirp/browser/templates/departmentpage.pt:3
+msgid "Courses"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:101
+#: waeup/sirp/browser/templates/facultymanagepage.pt:75
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:92
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:93
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:60
+msgid "Local Role"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:13
+#: waeup/sirp/browser/templates/departmentpage.pt:6
+msgid "Certificates"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:16
+#: waeup/sirp/browser/templates/userscontainerpage.pt:7
+#: waeup/sirp/browser/templates/facultymanagepage.pt:16
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:7
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:17
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:11
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:6
+#: waeup/sirp/browser/templates/facultymanagepage.pt:6
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:5
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:7
+msgid "Settings"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:97
+#: waeup/sirp/browser/templates/userscontainerpage.pt:4
+#: waeup/sirp/browser/templates/facultymanagepage.pt:71
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:88
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:89
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:56
+msgid "User Id"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:99
+#: waeup/sirp/browser/templates/userscontainerpage.pt:5
+#: waeup/sirp/browser/templates/facultymanagepage.pt:73
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:31
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:90
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:91
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:58
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:10
+#: waeup/sirp/hostels/browser_templates/containermanagepage.pt:11
+msgid "Name"
+msgstr ""
+
+#: waeup/sirp/browser/templates/facultypage.pt:1
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:1
+msgid "There no subobjects registered yet."
+msgstr ""
+
+#: waeup/sirp/browser/templates/loginpage.pt:13
+#: waeup/sirp/browser/templates/usereditformpage.pt:8
+#: waeup/sirp/students/browser_templates/basepage.pt:14
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:17
+#: waeup/sirp/students/browser_templates/change_password.pt:10
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:31
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:34
+msgid "Password:"
+msgstr ""
+
+#: waeup/sirp/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/sirp/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 Access Code (PWD) and inititialize your student account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/sirp/browser/templates/loginpage.pt:39
+msgid "login_trouble2"
+msgstr ""
+
+#. Default: "Or simply forgot your student id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/sirp/browser/templates/loginpage.pt:44
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/sirp/browser/templates/loginpage.pt:5
+msgid "User Name or Id:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/sirp/browser/templates/searchpage.pt:15
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:36
+msgid "Type"
+msgstr ""
+
+#: waeup/sirp/browser/templates/searchpage.pt:9
+#: waeup/sirp/students/browser_templates/containerpage.pt:21
+msgid "Search Results"
+msgstr ""
+
+#: waeup/sirp/browser/templates/showstudentspage.pt:5
+msgid "Fullname"
+msgstr ""
+
+#: waeup/sirp/browser/templates/showstudentspage.pt:6
+msgid "Status"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:101
+#: waeup/sirp/browser/templates/studentsitelayout.pt:86
+msgid "Form Status:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:113
+#: waeup/sirp/browser/templates/studentsitelayout.pt:99
+msgid "Back to top"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:114
+#: waeup/sirp/browser/templates/studentsitelayout.pt:100
+msgid ""
+"Student Information and Registration Portal<br /> Copyright &copy; WAeUP "
+"Group, 2008-2012"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:34
+#: waeup/sirp/browser/templates/studentsitelayout.pt:39
+msgid "Contact"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:40
+#: waeup/sirp/browser/templates/studentsitelayout.pt:45
+msgid "Logout"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:60
+msgid "Student Id:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:64
+msgid "State:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/usereditformpage.pt:14
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:23
+#: waeup/sirp/students/browser_templates/change_password.pt:16
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:37
+msgid "Retype password:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:170
+msgid "Add"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:383 waeup/sirp/students/viewlets.py:155
+msgid "Send email"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:434
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:448
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:473 waeup/sirp/browser/viewlets.py:497
+msgid "Show students"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:528 waeup/sirp/browser/viewlets.py:536
+msgid "Show batch logs"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:548
+msgid "Batch processing"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:557
+msgid "Upload CSV file"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:630
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:91 waeup/sirp/browser/viewlets.py:160
+#: waeup/sirp/students/viewlets.py:218 waeup/sirp/students/viewlets.py:403
+msgid "Edit"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:105
+msgid "Accommodation Section"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:113 waeup/sirp/hostels/browser.py:123
+msgid "Manage accommodation section"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:145 waeup/sirp/hostels/browser.py:158
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:161
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:170
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:172
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:207
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:210 waeup/sirp/hostels/browser.py:236
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:211 waeup/sirp/hostels/browser.py:250
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:212 waeup/sirp/hostels/browser.py:281
+msgid "Release selected beds"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:241
+msgid ""
+"${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:275
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:299
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:304
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:316
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:68 waeup/sirp/hostels/browser.py:78
+msgid "Hostels"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:95
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:9
+#: waeup/sirp/hostels/browser_templates/containermanagepage.pt:9
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:35
+msgid "Id"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:37
+msgid "Number"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:56
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:6
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:9
+msgid "Beds"
+msgstr ""
+
+#: waeup/sirp/hostels/hostel.py:180
+msgid "unreserved"
+msgstr ""
+
+#: waeup/sirp/hostels/hostel.py:187
+msgid "reserved"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:105
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:112
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:119
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:126
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:137
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:146
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:169
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:175 waeup/sirp/students/interfaces.py:427
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:181
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:186 waeup/sirp/hostels/interfaces.py:199
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:44
+msgid "Hostel Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:49
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:55
+msgid "Hostel Name"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:61
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:67
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:73
+msgid "Reserved Beds"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:84
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:91
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:98
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:57
+msgid "Bed A"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:58
+msgid "Bed B"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:59
+msgid "Bed C"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:60
+msgid "Bed D"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:61
+msgid "Bed E"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:62
+msgid "Bed F"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:63
+msgid "Bed G"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:64
+msgid "Bed H"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:65
+msgid "Bed I"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:69
+msgid "Block A"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:70
+msgid "Block B"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:71
+msgid "Block C"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:72
+msgid "Block D"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:73
+msgid "Block E"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:74
+msgid "Block F"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:75
+msgid "Block G"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:76
+msgid "Block H"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:77
+msgid "Block I"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:78
+msgid "Block K"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:79
+msgid "Block L"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:80
+msgid "Block M"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:81
+msgid "Block N"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:82
+msgid "Block O"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:83
+msgid "Block P"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:87
+msgid "Regular Hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:88
+msgid "Blocked Hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:89
+msgid "Postgraduate Hostel"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:72
+msgid "clearance started"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:73
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:74
+msgid "cleared"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:75
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:76
+msgid "returning"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:77
+msgid "courses registered"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:78
+msgid "courses validated"
+msgstr ""
+
+#: waeup/sirp/objecthistory.py:73
+msgid "by"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:106
+msgid "Surcharge 1"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:113
+msgid "Surcharge 2"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:120
+msgid "Surcharge 3"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:133
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:140
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:147
+msgid "Response Description"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:154
+msgid "Response Payment Reference"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:161
+msgid "Response Code"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:168
+msgid "Response Card Number"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:24
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:25
+msgid "Paid"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:26
+msgid "Failed"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:30
+msgid "School Fee"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:31
+msgid "Clearance"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:32
+msgid "Bed Allocation"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:33
+msgid "Hostel Maintenance"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:34
+msgid "Transfer"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:35
+msgid "Gown"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:36
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:51
+msgid "Payment Category"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:58
+msgid "Payment Item"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:64
+msgid "Payment State"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:71
+msgid "Ticket Creation Date"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:76
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:10
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:84
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "Payment Date"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:82
+msgid "Amount Authorized"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:94
+msgid "Payment Access Code"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1003 waeup/sirp/students/browser.py:1225
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1072
+msgid "This ticket has already been paid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1085 waeup/sirp/students/browser.py:1093
+#: waeup/sirp/students/browser.py:1101
+msgid "Valid callback received. ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1155
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1167
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:117 waeup/sirp/students/viewlets.py:114
+#: waeup/sirp/students/viewlets.py:534
+msgid "Study Course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1174
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1205
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1207
+msgid "Create bed ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1215
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1219
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:123 waeup/sirp/students/viewlets.py:120
+#: waeup/sirp/students/viewlets.py:535
+msgid "Payments"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1230
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1241 waeup/sirp/students/browser.py:1537
+#: waeup/sirp/students/browser.py:1650
+msgid "Activation code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1261 waeup/sirp/students/browser.py:1389
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1266 waeup/sirp/students/browser.py:1542
+#: waeup/sirp/students/browser.py:1655
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1269 waeup/sirp/students/browser.py:1545
+#: waeup/sirp/students/browser.py:1658 waeup/sirp/students/browser.py:1917
+msgid "invalidated"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1274 waeup/sirp/students/browser.py:1549
+#: waeup/sirp/students/browser.py:1663
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1285 waeup/sirp/students/browser.py:1403
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1294
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1312
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1328
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1334
+msgid "Bed Allocation: "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1361
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1367
+msgid "Student can't be relocated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:138 waeup/sirp/students/viewlets.py:126
+msgid "Accommodation"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1410
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1429
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1441 waeup/sirp/students/viewlets.py:356
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1456 waeup/sirp/students/viewlets.py:365
+msgid "Change password"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:147
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1470
+msgid "Password changed."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1482
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1497 waeup/sirp/students/viewlets.py:389
+#: waeup/sirp/students/workflow.py:44
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1501
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1518 waeup/sirp/students/browser.py:1639
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1522
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1526
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1553
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1565
+msgid "Edit clearance data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1577 waeup/sirp/students/browser.py:1591
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1585
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1600 waeup/sirp/students/workflow.py:58
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1601
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1604
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1613
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1622
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1631 waeup/sirp/students/viewlets.py:418
+msgid "Start course registration"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1635
+msgid "Start course registration now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1671
+msgid "Course registration has been started."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1690
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1699
+msgid "Create course list now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:170
+msgid "Student Section"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1738
+msgid "Add and remove course tickets of study level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1775
+msgid "Register course list"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1779
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1825
+msgid "Change my password"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1835
+msgid "Get new login credentials"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1849
+msgid "No student record found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1857
+msgid "You have successfully changed your password for the"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:186 waeup/sirp/students/browser.py:222
+msgid "Empty search string"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1862
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1873
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1876
+msgid "Set"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1891
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1899
+msgid "Access code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1902
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1908
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1914
+msgid "Access code has already been used."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1923
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:204 waeup/sirp/students/viewlets.py:139
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:228 waeup/sirp/students/browser.py:1888
+msgid "No student found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:255 waeup/sirp/students/viewlets.py:146
+msgid "Add student"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:263
+msgid "Student record created."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:279
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:322 waeup/sirp/students/browser.py:1865
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:333
+msgid "Manage base data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:402
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:418 waeup/sirp/students/viewlets.py:104
+#: waeup/sirp/students/viewlets.py:532
+#: waeup/sirp/students/browser_templates/clearanceeditpage.pt:6
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:424
+msgid "Clearance Slip of "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:444
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:473
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:475 waeup/sirp/students/browser.py:499
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:492
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:496
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:522
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:532
+msgid "Manage personal data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:552
+msgid "${a}: Study Course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:582
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:585 waeup/sirp/students/browser.py:626
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:586 waeup/sirp/students/browser.py:612
+msgid "Add study level"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:620
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:622 waeup/sirp/students/browser.py:1708
+msgid "This level exists."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:632
+msgid "No study level selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:668
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:691
+msgid "Level Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:697
+msgid "Course List"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:706
+msgid "Course Registration Slip  "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:712
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:35
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:12
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:33
+msgid "Sem."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:715
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:38
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:36
+msgid "Dept."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:716
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:16
+msgid "Faculty"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:717
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:40
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:38
+msgid "Cred."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:718
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:39
+msgid "Mand."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:719 waeup/sirp/students/interfaces.py:370
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:40
+msgid "Score"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:747 waeup/sirp/students/browser.py:776
+#: waeup/sirp/students/browser.py:861 waeup/sirp/students/browser.py:866
+#: waeup/sirp/students/browser.py:1748 waeup/sirp/students/browser.py:1798
+msgid "Add course ticket"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:768
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:786 waeup/sirp/students/browser.py:1758
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:810 waeup/sirp/students/browser.py:832
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:814
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:816 waeup/sirp/students/browser.py:844
+msgid "Student is in the wrong state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:837
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:841
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:881 waeup/sirp/students/browser.py:1811
+msgid "The ticket exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:883 waeup/sirp/students/browser.py:1813
+msgid "Successfully added ${a}."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:904
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:920
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:947
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:992
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:995
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:11
+msgid "Bed Category"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:29
+msgid "Registration Transition:"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/sirp/students/browser_templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/clearanceeditpage.pt:8
+msgid "Scans"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:28
+msgid "Select all"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:10
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:10
+msgid "with registration number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:11
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:11
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:12
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:12
+msgid "in session"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:13
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:13
+msgid "in department"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:14
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:14
+msgid "in study mode"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:30
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:36
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:31
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:37
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:32
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:38
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:14
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:25
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:87
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:54
+msgid "State"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:33
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:39
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:44
+msgid "Full Name"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:5
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:5
+msgid "for students"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:8
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:8
+msgid "with id"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:9
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:9
+msgid "with name"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:31
+msgid "Semester:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:39
+msgid "Credits:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:47
+msgid "Passmark:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/imageupload.pt:20
+#: waeup/sirp/students/browser_templates/fileupload.pt:27
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:25
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:11
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:85
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:52
+msgid "Category"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:12
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:86
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:53
+msgid "Item"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:8
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:82
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:49
+msgid "Payment Id"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:83
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:50
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/setpassword.pt:14
+msgid "Access Code:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/setpassword.pt:32
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/sirp/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/sirp/students/browser_templates/setpassword.pt:5
+msgid "Registration Number:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:6
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:9
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:52
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:30
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:57
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:35
+msgid "Level Code"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:58
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:36
+msgid "Level Title"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:71
+msgid "There are no levels registered yet."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveladdpage.pt:4
+msgid "Text to explain course registration procedure."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:15
+msgid "Department"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:6
+msgid "Course Tickets (Total Credits: ${DYNAMIC_CONTENT})"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:39
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:37
+msgid "Fact."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:43
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:19
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:41
+msgid "CO"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:44
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:42
+msgid "Auto"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:6
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:9
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:30
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:28
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:18
+msgid "Total Credits:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:64
+msgid "The're no tickets added yet."
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:176 waeup/sirp/students/interfaces.py:255
+#: waeup/sirp/students/interfaces.py:483
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:183 waeup/sirp/students/interfaces.py:265
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:191
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:198
+msgid "Email"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:218
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:223
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:231
+msgid "Nationality"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:241
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:275
+#: waeup/sirp/browser/templates/showstudentspage.pt:7
+msgid "Certificate"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:282
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:290
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:299
+#: waeup/sirp/browser/templates/showstudentspage.pt:8
+msgid "Current Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:307
+#: waeup/sirp/browser/templates/showstudentspage.pt:9
+msgid "Current Level"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:315
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:322
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:337 waeup/sirp/students/interfaces.py:434
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:8
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:13
+msgid "Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:344
+msgid "Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:363
+#: waeup/sirp/browser/templates/certificatepage.pt:30
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:33
+msgid "Mandatory"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:377
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:384
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:403
+#: waeup/sirp/university/interfaces.py:299
+#: waeup/sirp/browser/templates/certificatepage.pt:29
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:32
+msgid "Course"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:420
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:10
+msgid "Bed Coordinates"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:442
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:448
+msgid "Booking Activation Code"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/utils.py:163
+msgid "<font size=10>${a} Page ${b} of ${c}</font>"
+msgstr ""
+
+#: waeup/sirp/students/utils.py:276 waeup/sirp/students/viewlets.py:71
+#: waeup/sirp/students/viewlets.py:99
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:6
+msgid "Base Data"
+msgstr ""
+
+#: waeup/sirp/students/utils.py:307
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:109 waeup/sirp/students/viewlets.py:533
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:131 waeup/sirp/students/viewlets.py:537
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:27
+msgid "History"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:179 waeup/sirp/students/workflow.py:72
+msgid "Clear student"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:194
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:210
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:235
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:251 waeup/sirp/students/workflow.py:128
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:267
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:330
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:339
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:348
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:374
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:432
+msgid "Add course list"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:450
+msgid "Add and remove courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:536
+msgid "Accommodation Data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:548
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:561
+msgid "File name has no extension."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:564
+msgid "File name contains more than one dot."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:571
+msgid "${a} file extension expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:577
+msgid "Only the following extension are allowed: ${a}"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:583
+msgid "Uploaded file is too big."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:590
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:608
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:10
+#: waeup/sirp/browser/templates/datacenterpage.pt:14
+#: waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt:11
+msgid "File"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:609
+msgid "Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:632
+msgid "Upload new file"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:633
+msgid "Delete attachment"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:663
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:677
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:8
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:688
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:703 waeup/sirp/students/viewlets.py:714
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:704 waeup/sirp/students/viewlets.py:715
+msgid "Birth Certificate Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:724 waeup/sirp/students/viewlets.py:735
+msgid "Acceptance Letter"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:725 waeup/sirp/students/viewlets.py:736
+msgid "Acceptance Letter Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:85
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:135
+msgid "Male"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:137
+msgid "Female"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:77
+msgid "Error: level id ${value} out of range"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:88
+msgid "${title} 2nd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:91
+msgid "${title} 3rd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:94
+msgid "${title} 1st spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:96
+msgid "${title} on 1st probation"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:98
+msgid "${title} on 2nd probation"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:107
+msgid "Reset to returning"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:108
+msgid "Student record reset to 'returning'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:114
+msgid "Register courses"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:115
+msgid "Courses registered"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:121 waeup/sirp/students/workflow.py:135
+msgid "Reset to paid"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:122 waeup/sirp/students/workflow.py:136
+msgid "Student record reset to 'paid'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:129
+msgid "Courses validated"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:142
+msgid "Return"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:143
+msgid "Returned"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:149
+msgid "Reset to validated"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:150
+msgid "Student record reset to 'validated'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:22
+msgid "Create student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:30
+msgid "Admit student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:31
+msgid "Student admitted"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:37
+msgid "Reset student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:38
+msgid "Student record reset"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:45
+msgid "Clearance started"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:51
+msgid "Reset to admitted"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:52
+msgid "Student record reset to 'admitted'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:59
+msgid "Clearance requested"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:65 waeup/sirp/students/workflow.py:79
+msgid "Reset to clearance"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:66 waeup/sirp/students/workflow.py:80
+msgid "Student record reset to 'clearance'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:73
+msgid "Cleared"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:86 waeup/sirp/students/workflow.py:100
+msgid "Pay school fee"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:87 waeup/sirp/students/workflow.py:101
+msgid "School fee paid"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:93
+msgid "Reset to cleared"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:94
+msgid "Student record reset to 'cleared'"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:146
+msgid "Title of course"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:152
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:17
+msgid "Credits"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:158
+msgid "Passmark"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:164
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:180
+msgid "Enter unique course code which will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:205
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:212
+msgid "Start Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:219
+msgid "End Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:226
+msgid "Application Category"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:243
+msgid "Enter unique certificate code which will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:264
+msgid "Course Referrer"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:270
+#: waeup/sirp/browser/templates/certificatepage.pt:28
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:30
+msgid "Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:277
+msgid "Is mandatory course (not elective)"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:45
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:46
+#: waeup/sirp/university/interfaces.py:94
+#: waeup/sirp/university/interfaces.py:147
+#: waeup/sirp/university/interfaces.py:200
+msgid "Unnamed"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:51
+#: waeup/sirp/university/interfaces.py:99
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:67
+#: waeup/sirp/university/interfaces.py:118
+msgid "This code will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:93
+msgid "Name of department"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:28
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:29
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:30
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:31
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:32
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:33
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:34
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:35
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:36
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/sirp/utils/utils.py:119
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/sirp/utils/utils.py:164
+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"
+"Login page: ${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 ""
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/waeup.sirp.pot
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/waeup.sirp.pot	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/waeup.sirp.pot	(revision 7760)
@@ -0,0 +1,3770 @@
+##############################################################################
+#
+# 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: Fri Mar  2 12:58:47 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"
+"Generated-By: zope/app/locales/extract.py\n"
+
+#: waeup/sirp/accesscodes/browser.py:100
+#: waeup/sirp/accesscodes/browser.py:113
+#: waeup/sirp/applicants/browser.py:80
+#: waeup/sirp/applicants/browser.py:130
+#: waeup/sirp/applicants/browser.py:182
+#: waeup/sirp/applicants/browser.py:254
+#: waeup/sirp/applicants/browser.py:255
+#: waeup/sirp/applicants/browser.py:338
+#: waeup/sirp/browser/pages.py:503
+#: waeup/sirp/browser/pages.py:708
+#: waeup/sirp/browser/pages.py:735
+#: waeup/sirp/browser/pages.py:758
+#: waeup/sirp/browser/pages.py:805
+#: waeup/sirp/browser/pages.py:835
+#: waeup/sirp/browser/pages.py:955
+#: waeup/sirp/browser/pages.py:1183
+#: waeup/sirp/browser/pages.py:1277
+#: waeup/sirp/browser/pages.py:1298
+#: waeup/sirp/browser/pages.py:1325
+#: waeup/sirp/browser/pages.py:1353
+#: waeup/sirp/browser/pages.py:1354
+#: waeup/sirp/browser/pages.py:1400
+#: waeup/sirp/browser/pages.py:1441
+#: waeup/sirp/browser/pages.py:1520
+#: waeup/sirp/browser/pages.py:1521
+#: waeup/sirp/browser/pages.py:1523
+#: waeup/sirp/browser/pages.py:1601
+#: waeup/sirp/browser/pages.py:1646
+#: waeup/sirp/browser/pages.py:1684
+#: waeup/sirp/browser/pages.py:1728
+#: waeup/sirp/browser/pages.py:1767
+#: waeup/sirp/browser/pages.py:1769
+#: waeup/sirp/browser/pages.py:1804
+#: waeup/sirp/browser/pages.py:1855
+#: waeup/sirp/browser/pages.py:1907
+#: waeup/sirp/students/browser.py:584
+#: waeup/sirp/students/browser.py:585
+#: waeup/sirp/students/browser.py:746
+#: waeup/sirp/students/browser.py:748
+#: waeup/sirp/students/browser.py:888
+msgid "Cancel"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:102
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:112
+msgid "Reimport"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:115
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:120
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:126
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:136
+#. Default: ""
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:139
+#. Default: ""
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:152
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:153
+#: waeup/sirp/browser/pages.py:592
+#: waeup/sirp/browser/pages.py:1142
+#: waeup/sirp/browser/viewlets.py:190
+#: waeup/sirp/students/browser.py:171
+#: waeup/sirp/students/browser.py:205
+msgid "Search"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:154
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:155
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:180
+#: waeup/sirp/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:182
+#. Default: ""
+msgid "${a} disabled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:186
+#. Default: ""
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:190
+msgid "re-enabled"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:192
+#. Default: ""
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:196
+#. Default: ""
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:235
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:244
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:252
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:261
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:43
+msgid "Archive"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:44
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:46
+#: waeup/sirp/accesscodes/browser.py:206
+#: waeup/sirp/accesscodes/browser.py:226
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:53
+msgid "No batch selected."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:61
+#. Default: ""
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:68
+#. Default: ""
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:77
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:83
+msgid "Create batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:91
+#. Default: ""
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:93
+#. Default: ""
+msgid "Data written to ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:10
+msgid "Entries/(invalidated)"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:11
+msgid "Cost"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:12
+msgid "Date of Creation"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:13
+msgid "Creator"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:2
+msgid "The following batches are available:"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:39
+msgid "No batches yet"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:9
+msgid "Prefix"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt:23
+msgid "No import batches available"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:23
+msgid "Serial"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:24
+msgid "AC"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:26
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:38
+msgid "Owner"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:5
+msgid "for access codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:7
+msgid "with PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:8
+msgid "with serial"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:9
+msgid "with message term"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:29
+msgid "Serial number inside batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:32
+msgid "Prefix inside batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:35
+msgid "Batch number"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:38
+msgid "Random part of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:41
+#: waeup/sirp/accesscodes/interfaces.py:80
+msgid "Cost of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:45
+msgid "Workflow state"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:48
+msgid "Complete title of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:51
+msgid "Purchaser"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:54
+msgid "The history of access code as lines"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:63
+msgid "Creation date"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:66
+msgid "Batch creator"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:69
+msgid "Batch prefix"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:72
+msgid "Batch number (1-3 digits)"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:76
+msgid "Number of access codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:84
+msgid "Number of disabled access codes inside the batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:89
+msgid "Number of used access codes inside the batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:102
+msgid "Disable used PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:109
+msgid "Reenable disabled PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:37
+#: waeup/sirp/applicants/workflow.py:37
+msgid "initialized"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:38
+msgid "used"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:81
+msgid "Initialize PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:88
+msgid "Use PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:95
+msgid "Disable unused PIN"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:117
+#: waeup/sirp/applicants/browser.py:325
+#: waeup/sirp/applicants/browser.py:745
+msgid "Could not delete:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:120
+#: waeup/sirp/applicants/browser.py:328
+#: waeup/sirp/applicants/browser.py:748
+#: waeup/sirp/browser/pages.py:134
+#: waeup/sirp/students/browser.py:243
+#: waeup/sirp/students/browser.py:642
+#: waeup/sirp/students/browser.py:796
+#: waeup/sirp/students/browser.py:974
+#: waeup/sirp/students/browser.py:1184
+#: waeup/sirp/students/browser.py:1770
+#. Default: ""
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:169
+msgid "An applicants container for the same application type and entrance year exists already in the database."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:178
+msgid "Added:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:190
+#: waeup/sirp/applicants/viewlets.py:44
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:12
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:36
+msgid "Applicants"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:254
+#: waeup/sirp/applicants/browser.py:301
+#: waeup/sirp/applicants/browser.py:654
+#: waeup/sirp/applicants/browser.py:688
+#: waeup/sirp/applicants/browser.py:791
+#: waeup/sirp/applicants/browser.py:794
+#: waeup/sirp/applicants/browser.py:838
+#: waeup/sirp/browser/pages.py:476
+#: waeup/sirp/browser/pages.py:631
+#: waeup/sirp/browser/pages.py:654
+#: waeup/sirp/browser/pages.py:728
+#: waeup/sirp/browser/pages.py:1181
+#: waeup/sirp/browser/pages.py:1353
+#: waeup/sirp/browser/pages.py:1394
+#: waeup/sirp/browser/pages.py:1520
+#: waeup/sirp/browser/pages.py:1573
+#: waeup/sirp/browser/pages.py:1716
+#: waeup/sirp/browser/pages.py:1767
+#: waeup/sirp/browser/pages.py:1788
+#: waeup/sirp/hostels/browser.py:209
+#: waeup/sirp/hostels/browser.py:231
+#: waeup/sirp/hostels/browser.py:319
+#: waeup/sirp/students/browser.py:359
+#: waeup/sirp/students/browser.py:458
+#: waeup/sirp/students/browser.py:535
+#: waeup/sirp/students/browser.py:584
+#: waeup/sirp/students/browser.py:600
+#: waeup/sirp/students/browser.py:746
+#: waeup/sirp/students/browser.py:771
+#: waeup/sirp/students/browser.py:1444
+#: waeup/sirp/students/browser.py:1459
+#: waeup/sirp/students/browser.py:1574
+msgid "Save"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:255
+#: waeup/sirp/applicants/browser.py:333
+#: waeup/sirp/applicants/browser.py:361
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:265
+#: waeup/sirp/applicants/viewlets.py:95
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:305
+#: waeup/sirp/applicants/browser.py:717
+#: waeup/sirp/browser/pages.py:1397
+#: waeup/sirp/browser/pages.py:1719
+#: waeup/sirp/browser/pages.py:1791
+#: waeup/sirp/browser/pages.py:1903
+#: waeup/sirp/hostels/browser.py:54
+#: waeup/sirp/students/browser.py:80
+#: waeup/sirp/students/browser.py:385
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:314
+msgid "No applicant selected!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:364
+msgid "Create application record"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:369
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:382
+msgid "Applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:397
+#: waeup/sirp/students/browser.py:285
+msgid "set"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:398
+#: waeup/sirp/students/browser.py:286
+msgid "unset"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:403
+#. Default: ""
+msgid "${a} Application Record ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:454
+msgid "Session configuration object is not available."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:461
+#: waeup/sirp/students/browser.py:1023
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:474
+#: waeup/sirp/students/browser.py:1037
+msgid "Payment ticket created."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:501
+#: waeup/sirp/students/browser.py:1056
+#. Default: ""
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:529
+#: waeup/sirp/students/browser.py:1105
+msgid "Valid callback received."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:550
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:556
+msgid "Online Payment Receipt"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:562
+msgid "Ticket not yet paid."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:627
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:632
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:64
+msgid "Application Section"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:654
+#: waeup/sirp/applicants/browser.py:794
+#: waeup/sirp/applicants/browser.py:846
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:655
+#: waeup/sirp/applicants/browser.py:758
+#: waeup/sirp/applicants/browser.py:792
+#: waeup/sirp/applicants/browser.py:827
+#: waeup/sirp/students/browser.py:980
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:655
+#: waeup/sirp/applicants/browser.py:763
+#: waeup/sirp/applicants/browser.py:792
+#: waeup/sirp/applicants/browser.py:795
+#: waeup/sirp/applicants/browser.py:832
+#: waeup/sirp/students/browser.py:748
+#: waeup/sirp/students/browser.py:780
+#: waeup/sirp/students/browser.py:956
+#: waeup/sirp/students/browser.py:1752
+msgid "Remove selected tickets"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:674
+#. Default: ""
+msgid "${a} Application Form ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:685
+#: waeup/sirp/students/browser.py:356
+msgid "No transition"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:732
+#: waeup/sirp/students/browser.py:962
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:76
+#: waeup/sirp/applicants/viewlets.py:88
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:79
+#: waeup/sirp/applicants/browser.py:105
+#: waeup/sirp/applicants/browser.py:255
+#: waeup/sirp/applicants/browser.py:308
+#: waeup/sirp/browser/pages.py:634
+#: waeup/sirp/browser/pages.py:675
+#: waeup/sirp/browser/pages.py:1277
+#: waeup/sirp/browser/pages.py:1288
+#: waeup/sirp/browser/pages.py:1354
+#: waeup/sirp/browser/pages.py:1389
+#: waeup/sirp/hostels/browser.py:131
+#: waeup/sirp/students/browser.py:206
+#: waeup/sirp/students/browser.py:1151
+#: waeup/sirp/students/browser.py:1164
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:79
+#: waeup/sirp/applicants/browser.py:125
+#: waeup/sirp/applicants/browser.py:148
+#: waeup/sirp/applicants/browser.py:160
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:805
+#: waeup/sirp/students/browser.py:88
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:81
+#: waeup/sirp/applicants/browser.py:139
+#: waeup/sirp/applicants/browser.py:256
+#: waeup/sirp/applicants/browser.py:347
+#: waeup/sirp/browser/pages.py:1355
+#: waeup/sirp/browser/pages.py:1414
+#: waeup/sirp/browser/pages.py:1524
+#: waeup/sirp/browser/pages.py:1610
+#: waeup/sirp/browser/pages.py:1770
+#: waeup/sirp/browser/pages.py:1827
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:819
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:82
+#: waeup/sirp/applicants/browser.py:135
+#: waeup/sirp/applicants/browser.py:257
+#: waeup/sirp/applicants/browser.py:343
+#: waeup/sirp/browser/pages.py:1356
+#: waeup/sirp/browser/pages.py:1410
+#: waeup/sirp/browser/pages.py:1525
+#: waeup/sirp/browser/pages.py:1606
+#: waeup/sirp/browser/pages.py:1771
+#: waeup/sirp/browser/pages.py:1823
+msgid "Add local role"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:821
+msgid "Passport picture confirmation box not ticked."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:83
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:858
+#: waeup/sirp/students/browser.py:1619
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:863
+msgid "Form has been submitted."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:898
+#. Default: ""
+msgid "Register for ${a} Application"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:904
+msgid "Application has not yet started."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:909
+msgid "Application has ended."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:918
+msgid "Get login credentials"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:933
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:940
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:949
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:18
+msgid "Photograph"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:43
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:59
+msgid "I confirm that the Passport Photograph uploaded on this form is a true picture of me."
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:74
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:43
+msgid "Acceptance Fee Payment Tickets"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:11
+msgid "Thanks for using SIRP!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:3
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/sirp/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/sirp/applicants/browser_templates/applicantregister.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:42
+msgid "Application Number"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:46
+msgid "First Choice"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:48
+msgid "Application State"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:4
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:23
+msgid "Year"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:7
+msgid "Application Period"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:197
+#: waeup/sirp/students/browser.py:713
+#: waeup/sirp/university/interfaces.py:38
+#: waeup/sirp/university/interfaces.py:66
+#: waeup/sirp/university/interfaces.py:86
+#: waeup/sirp/university/interfaces.py:117
+#: waeup/sirp/university/interfaces.py:139
+#: waeup/sirp/university/interfaces.py:179
+#: waeup/sirp/university/interfaces.py:192
+#: waeup/sirp/university/interfaces.py:241
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:39
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:68
+#: waeup/sirp/browser/templates/facultypage.pt:8
+#: waeup/sirp/browser/templates/departmentpage.pt:15
+#: waeup/sirp/browser/templates/departmentpage.pt:39
+#: waeup/sirp/browser/templates/searchpage.pt:13
+#: waeup/sirp/browser/templates/facultymanagepage.pt:41
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:36
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:13
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:34
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:5
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:25
+msgid "Code"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:204
+#: waeup/sirp/students/browser.py:714
+#: waeup/sirp/university/interfaces.py:199
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:41
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:70
+#: waeup/sirp/browser/templates/facultypage.pt:9
+#: waeup/sirp/browser/templates/departmentpage.pt:16
+#: waeup/sirp/browser/templates/departmentpage.pt:40
+#: waeup/sirp/browser/templates/searchpage.pt:14
+#: waeup/sirp/browser/templates/certificatepage.pt:30
+#: waeup/sirp/browser/templates/facultymanagepage.pt:43
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:32
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:37
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:35
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:6
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:27
+msgid "Title"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:211
+#: waeup/sirp/applicants/interfaces.py:324
+msgid "Application Target"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:219
+msgid "Entry Level"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:226
+#: waeup/sirp/applicants/interfaces.py:332
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:234
+#: waeup/sirp/applicants/interfaces.py:340
+msgid "Applicants Container Type"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:250
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:257
+msgid "Human readable description in reST format"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:273
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:279
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:285
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:368
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:373
+msgid "JAMB Registration Number"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:385
+#: waeup/sirp/students/interfaces.py:154
+msgid "First Name"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:389
+#: waeup/sirp/students/interfaces.py:159
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:393
+#: waeup/sirp/students/interfaces.py:164
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:397
+#: waeup/sirp/students/interfaces.py:213
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:402
+msgid "State/LGA"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:407
+#: waeup/sirp/students/interfaces.py:169
+msgid "Sex"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:413
+#: waeup/sirp/students/interfaces.py:490
+msgid "Email Address"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:418
+#: waeup/sirp/students/interfaces.py:203
+msgid "Phone"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:423
+#: waeup/sirp/applicants/interfaces.py:503
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:428
+#: waeup/sirp/applicants/interfaces.py:508
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:437
+#: waeup/sirp/applicants/interfaces.py:513
+msgid "Screening Score"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:441
+#: waeup/sirp/applicants/interfaces.py:518
+msgid "Screening Venue"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:445
+#: waeup/sirp/applicants/interfaces.py:523
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:451
+#: waeup/sirp/applicants/interfaces.py:530
+msgid "Notice"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:473
+#: waeup/sirp/students/interfaces.py:149
+#: waeup/sirp/browser/templates/showstudentspage.pt:4
+#: waeup/sirp/students/browser_templates/containerpage.pt:29
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:35
+msgid "Student Id"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:478
+msgid "Form locked"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:557
+#: waeup/sirp/students/interfaces.py:469
+msgid "Payment Session"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:110
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:23
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:121
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:15
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:182
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:26
+msgid "Admitted Course of Study:"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:200
+#. Default: ""
+msgid "<font size=10>Proceed to the login page of the portal and enter your new credentials: user name= ${a}, password = ${b}.</font>"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:206
+msgid "<font size=10>Change your password when you have  logged in.</font>"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:57
+msgid "Application Record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:103
+msgid "Register for application"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:111
+msgid "View application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:119
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:127
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:143
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:151
+#: waeup/sirp/applicants/workflow.py:99
+#: waeup/sirp/students/browser.py:258
+msgid "Create student record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:168
+#: waeup/sirp/students/viewlets.py:300
+msgid "Download payment receipt"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:183
+#: waeup/sirp/students/viewlets.py:315
+msgid "Request callback"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:57
+msgid "Application"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:73
+#: waeup/sirp/students/viewlets.py:512
+msgid "My Data"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:100
+#: waeup/sirp/students/workflow.py:25
+msgid "Student record created"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:106
+#: waeup/sirp/applicants/workflow.py:113
+#: waeup/sirp/applicants/workflow.py:120
+#: waeup/sirp/applicants/workflow.py:127
+msgid "Reset application to started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:107
+#: waeup/sirp/applicants/workflow.py:114
+#: waeup/sirp/applicants/workflow.py:121
+#: waeup/sirp/applicants/workflow.py:128
+#: waeup/sirp/applicants/workflow.py:134
+msgid "Application reset"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:133
+msgid "Reset application to paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:38
+msgid "started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:39
+msgid "paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:40
+msgid "submitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:41
+#: waeup/sirp/interfaces.py:71
+msgid "admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:42
+msgid "not admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:43
+#: waeup/sirp/interfaces.py:70
+msgid "created"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:49
+msgid "Initialize application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:52
+msgid "Application initialized"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:57
+msgid "Start application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:58
+msgid "Application started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:64
+msgid "Pay acceptance fee"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:65
+msgid "Fee paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:71
+msgid "Submit application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:72
+msgid "Application submitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:78
+msgid "Admit applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:79
+msgid "Applicant admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:85
+#: waeup/sirp/applicants/workflow.py:92
+msgid "Refuse application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:86
+#: waeup/sirp/applicants/workflow.py:93
+msgid "Application refused"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:104
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:116
+#: waeup/sirp/browser/viewlets.py:615
+msgid "Academics"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:123
+#: waeup/sirp/browser/pages.py:309
+msgid "Administration"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:130
+#: waeup/sirp/browser/viewlets.py:360
+#: waeup/sirp/browser/viewlets.py:666
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:148
+#: waeup/sirp/browser/pages.py:386
+#: waeup/sirp/browser/viewlets.py:375
+#: waeup/sirp/browser/viewlets.py:676
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:155
+#: waeup/sirp/browser/pages.py:748
+#: waeup/sirp/browser/pages.py:1140
+#: waeup/sirp/browser/viewlets.py:393
+#: waeup/sirp/browser/viewlets.py:685
+msgid "Data Center"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:90
+msgid "Home"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1016
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:103
+#. Default: ""
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1063
+msgid "View processing log"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1064
+msgid "Back to data center"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1108
+#. Default: ""
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1110
+#. Default: ""
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1118
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1120
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1121
+msgid "Show"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:113
+#: waeup/sirp/hostels/browser.py:256
+#: waeup/sirp/hostels/browser.py:287
+#: waeup/sirp/students/browser.py:230
+msgid "No item selected."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1143
+msgid "Back"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1151
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1179
+msgid "Edit data center settings"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1199
+#. Default: ""
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1202
+msgid "Given storage path cannot be used."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1203
+#. Default: ""
+msgid "Error: ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1206
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1265
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1277
+#: waeup/sirp/browser/pages.py:1293
+#: waeup/sirp/browser/pages.py:1310
+#: waeup/sirp/browser/pages.py:1314
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1278
+msgid "Faculties"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1282
+#: waeup/sirp/browser/viewlets.py:441
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1321
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1342
+#: waeup/sirp/browser/pages.py:1352
+msgid "Departments"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1354
+#: waeup/sirp/browser/pages.py:1405
+#: waeup/sirp/browser/pages.py:1424
+#: waeup/sirp/browser/pages.py:1428
+msgid "Add department"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1362
+#: waeup/sirp/browser/viewlets.py:457
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1435
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1438
+#. Default: ""
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1455
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1485
+#: waeup/sirp/browser/pages.py:1504
+#: waeup/sirp/students/browser.py:96
+#: waeup/sirp/students/viewlets.py:472
+msgid "Students"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:152
+#: waeup/sirp/browser/viewlets.py:322
+#: waeup/sirp/browser/templates/staffsitelayout.pt:28
+#: waeup/sirp/browser/templates/studentsitelayout.pt:33
+msgid "Login"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1521
+#: waeup/sirp/browser/pages.py:1579
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1521
+#: waeup/sirp/browser/pages.py:1591
+#: waeup/sirp/browser/pages.py:1620
+#: waeup/sirp/browser/pages.py:1624
+msgid "Add course"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1522
+#: waeup/sirp/browser/pages.py:1585
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1522
+#: waeup/sirp/browser/pages.py:1596
+#: waeup/sirp/browser/pages.py:1657
+#: waeup/sirp/browser/pages.py:1661
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1531
+#: waeup/sirp/browser/viewlets.py:464
+msgid "Manage department"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:160
+msgid "You logged in."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1636
+msgid "A course with same code already exists: "
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1642
+#. Default: ""
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1673
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1679
+#. Default: ""
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1711
+#: waeup/sirp/browser/viewlets.py:481
+msgid "Edit course"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1761
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1768
+#: waeup/sirp/browser/pages.py:1799
+#: waeup/sirp/browser/pages.py:1840
+#: waeup/sirp/browser/pages.py:1842
+msgid "Add course referrer"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1769
+#: waeup/sirp/browser/pages.py:1794
+msgid "Remove selected course referrers"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1775
+#: waeup/sirp/browser/viewlets.py:488
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:178
+msgid "You entered wrong credentials."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1847
+msgid "The chosen course referrer is already part of this certificate."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1851
+#. Default: ""
+msgid "Course referrer ${a}_${b} added."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1888
+#: waeup/sirp/browser/viewlets.py:505
+msgid "Edit course referrer"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1891
+msgid "Save and return"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:192
+msgid "You have been logged out. Thanks for using WAeUP SIRP!"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:228
+#. Default: ""
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:234
+#: waeup/sirp/browser/pages.py:519
+msgid "Send message now"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:253
+#: waeup/sirp/browser/pages.py:272
+#: waeup/sirp/browser/pages.py:536
+#: waeup/sirp/students/browser.py:320
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:263
+msgid "Send now"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:274
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:299
+msgid "<h1>Welcome to WAeUP.SIRP</h1>"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:387
+#: waeup/sirp/hostels/browser.py:195
+#: waeup/sirp/students/viewlets.py:163
+#: waeup/sirp/students/viewlets.py:171
+#: waeup/sirp/students/viewlets.py:226
+#: waeup/sirp/students/viewlets.py:243
+#: waeup/sirp/students/viewlets.py:283
+msgid "Manage"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:388
+#: waeup/sirp/browser/viewlets.py:180
+msgid "Remove"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:395
+#. Default: ""
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:426
+#: waeup/sirp/browser/pages.py:428
+#: waeup/sirp/browser/viewlets.py:515
+msgid "Add user"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:450
+#. Default: ""
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:469
+#. Default: ""
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:500
+msgid "User settings have been saved."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:517
+#: waeup/sirp/students/browser.py:301
+#. Default: ""
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:546
+#: waeup/sirp/browser/viewlets.py:400
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:559
+#: waeup/sirp/browser/viewlets.py:415
+msgid "My Roles"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:590
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:617
+msgid "View portal configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:630
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:631
+#: waeup/sirp/browser/pages.py:680
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:633
+#: waeup/sirp/browser/pages.py:661
+#: waeup/sirp/browser/pages.py:692
+msgid "Add session configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:658
+#: waeup/sirp/browser/pages.py:731
+msgid "Settings have been saved."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:672
+#. Default: ""
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:683
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:696
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:703
+msgid "The session chosen already exists."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:725
+#. Default: ""
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:755
+msgid "Upload file"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:757
+msgid "Upload"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:803
+#: waeup/sirp/browser/pages.py:833
+#: waeup/sirp/browser/pages.py:953
+#: waeup/sirp/browser/pages.py:1061
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:817
+#: waeup/sirp/browser/pages.py:918
+#: waeup/sirp/browser/pages.py:1037
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:83
+msgid "No local role selected."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:836
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:837
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:897
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:905
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:930
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:956
+#: waeup/sirp/browser/pages.py:1182
+msgid "Reset"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:957
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:958
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:959
+msgid "Perform import"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:10
+msgid "Course Code:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:14
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:7
+msgid "Course Title:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:18
+msgid "Provided by:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:26
+msgid "Level:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:30
+msgid "Is mandatory course (not elective):"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:6
+msgid "Code:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:22
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:6
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:26
+msgid "Course Referrers"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:28
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:31
+msgid "Semester"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:29
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:31
+msgid "Referrer"
+msgstr ""
+
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr ""
+
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:8
+msgid "Session Configurations"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:14
+msgid "Datasets"
+msgstr ""
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:43
+msgid "Mode:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:8
+msgid "Please select a file-processor and a processing-mode from the selections below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:12
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:6
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:4
+msgid "File:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:18
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:9
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:32
+msgid "Processor:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:24
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:13
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:44
+msgid "change to:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:12
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:16
+msgid "Date"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:14
+#: waeup/sirp/browser/templates/datacenterpage.pt:15
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:12
+msgid "Overwrite contents in new location?"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:3
+#: waeup/sirp/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:7
+msgid "Copy existing data to new storage?"
+msgstr ""
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterpage.pt:16
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:10
+#: waeup/sirp/browser/templates/departmentpage.pt:3
+msgid "Courses"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:101
+#: waeup/sirp/browser/templates/facultymanagepage.pt:75
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:92
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:93
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:60
+msgid "Local Role"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:13
+#: waeup/sirp/browser/templates/departmentpage.pt:6
+msgid "Certificates"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:16
+#: waeup/sirp/browser/templates/userscontainerpage.pt:7
+#: waeup/sirp/browser/templates/facultymanagepage.pt:16
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:7
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:17
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:11
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:6
+#: waeup/sirp/browser/templates/facultymanagepage.pt:6
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:5
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:7
+msgid "Settings"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:97
+#: waeup/sirp/browser/templates/userscontainerpage.pt:4
+#: waeup/sirp/browser/templates/facultymanagepage.pt:71
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:88
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:89
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:56
+msgid "User Id"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:99
+#: waeup/sirp/browser/templates/userscontainerpage.pt:5
+#: waeup/sirp/browser/templates/facultymanagepage.pt:73
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:31
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:90
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:91
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:58
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:10
+#: waeup/sirp/hostels/browser_templates/containermanagepage.pt:11
+msgid "Name"
+msgstr ""
+
+#: waeup/sirp/browser/templates/facultypage.pt:1
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:1
+msgid "There no subobjects registered yet."
+msgstr ""
+
+#: waeup/sirp/browser/templates/loginpage.pt:13
+#: waeup/sirp/browser/templates/usereditformpage.pt:8
+#: waeup/sirp/students/browser_templates/basepage.pt:14
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:17
+#: waeup/sirp/students/browser_templates/change_password.pt:10
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:31
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:34
+msgid "Password:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/loginpage.pt:24
+msgid "Notice: User names, Ids and passwords are case sensitive."
+msgstr ""
+
+#: waeup/sirp/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/sirp/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 Access Code (PWD) and inititialize your student account <strong><a href=\"setpassword\"> here</a></strong>."
+msgid "login_trouble2"
+msgstr ""
+
+#: waeup/sirp/browser/templates/loginpage.pt:44
+#. Default: "Or simply forgot your student id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/sirp/browser/templates/loginpage.pt:5
+msgid "User Name or Id:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/sirp/browser/templates/searchpage.pt:15
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:36
+msgid "Type"
+msgstr ""
+
+#: waeup/sirp/browser/templates/searchpage.pt:9
+#: waeup/sirp/students/browser_templates/containerpage.pt:21
+msgid "Search Results"
+msgstr ""
+
+#: waeup/sirp/browser/templates/showstudentspage.pt:5
+msgid "Fullname"
+msgstr ""
+
+#: waeup/sirp/browser/templates/showstudentspage.pt:6
+msgid "Status"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:101
+#: waeup/sirp/browser/templates/studentsitelayout.pt:86
+msgid "Form Status:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:113
+#: waeup/sirp/browser/templates/studentsitelayout.pt:99
+msgid "Back to top"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:114
+#: waeup/sirp/browser/templates/studentsitelayout.pt:100
+msgid "Student Information and Registration Portal<br /> Copyright &copy; WAeUP Group, 2008-2012"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:34
+#: waeup/sirp/browser/templates/studentsitelayout.pt:39
+msgid "Contact"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:40
+#: waeup/sirp/browser/templates/studentsitelayout.pt:45
+msgid "Logout"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:60
+msgid "Student Id:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:64
+msgid "State:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/usereditformpage.pt:14
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:23
+#: waeup/sirp/students/browser_templates/change_password.pt:16
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:37
+msgid "Retype password:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:170
+msgid "Add"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:383
+#: waeup/sirp/students/viewlets.py:155
+msgid "Send email"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:434
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:448
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:473
+#: waeup/sirp/browser/viewlets.py:497
+msgid "Show students"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:528
+#: waeup/sirp/browser/viewlets.py:536
+msgid "Show batch logs"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:548
+msgid "Batch processing"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:557
+msgid "Upload CSV file"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:630
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:91
+#: waeup/sirp/browser/viewlets.py:160
+#: waeup/sirp/students/viewlets.py:218
+#: waeup/sirp/students/viewlets.py:403
+msgid "Edit"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:105
+msgid "Accommodation Section"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:113
+#: waeup/sirp/hostels/browser.py:123
+msgid "Manage accommodation section"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:145
+#: waeup/sirp/hostels/browser.py:158
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:161
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:170
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:172
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:207
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:210
+#: waeup/sirp/hostels/browser.py:236
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:211
+#: waeup/sirp/hostels/browser.py:250
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:212
+#: waeup/sirp/hostels/browser.py:281
+msgid "Release selected beds"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:241
+#. Default: ""
+msgid "${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:275
+#. Default: ""
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:299
+#. Default: ""
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:304
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:316
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:68
+#: waeup/sirp/hostels/browser.py:78
+msgid "Hostels"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:95
+#. Default: ""
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:9
+#: waeup/sirp/hostels/browser_templates/containermanagepage.pt:9
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:35
+msgid "Id"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:37
+msgid "Number"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:56
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:6
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:9
+msgid "Beds"
+msgstr ""
+
+#: waeup/sirp/hostels/hostel.py:180
+msgid "unreserved"
+msgstr ""
+
+#: waeup/sirp/hostels/hostel.py:187
+msgid "reserved"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:105
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:112
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:119
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:126
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:137
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:146
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:169
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:175
+#: waeup/sirp/students/interfaces.py:427
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:181
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:186
+#: waeup/sirp/hostels/interfaces.py:199
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:44
+msgid "Hostel Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:49
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:55
+msgid "Hostel Name"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:61
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:67
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:73
+msgid "Reserved Beds"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:84
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:91
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:98
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:57
+msgid "Bed A"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:58
+msgid "Bed B"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:59
+msgid "Bed C"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:60
+msgid "Bed D"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:61
+msgid "Bed E"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:62
+msgid "Bed F"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:63
+msgid "Bed G"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:64
+msgid "Bed H"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:65
+msgid "Bed I"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:69
+msgid "Block A"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:70
+msgid "Block B"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:71
+msgid "Block C"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:72
+msgid "Block D"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:73
+msgid "Block E"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:74
+msgid "Block F"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:75
+msgid "Block G"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:76
+msgid "Block H"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:77
+msgid "Block I"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:78
+msgid "Block K"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:79
+msgid "Block L"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:80
+msgid "Block M"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:81
+msgid "Block N"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:82
+msgid "Block O"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:83
+msgid "Block P"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:87
+msgid "Regular Hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:88
+msgid "Blocked Hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:89
+msgid "Postgraduate Hostel"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:72
+msgid "clearance started"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:73
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:74
+msgid "cleared"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:75
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:76
+msgid "returning"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:77
+msgid "courses registered"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:78
+msgid "courses validated"
+msgstr ""
+
+#: waeup/sirp/objecthistory.py:73
+msgid "by"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:106
+msgid "Surcharge 1"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:113
+msgid "Surcharge 2"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:120
+msgid "Surcharge 3"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:133
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:140
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:147
+msgid "Response Description"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:154
+msgid "Response Payment Reference"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:161
+msgid "Response Code"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:168
+msgid "Response Card Number"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:24
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:25
+msgid "Paid"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:26
+msgid "Failed"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:30
+msgid "School Fee"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:31
+msgid "Clearance"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:32
+msgid "Bed Allocation"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:33
+msgid "Hostel Maintenance"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:34
+msgid "Transfer"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:35
+msgid "Gown"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:36
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:51
+msgid "Payment Category"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:58
+msgid "Payment Item"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:64
+msgid "Payment State"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:71
+msgid "Ticket Creation Date"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:76
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:10
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:84
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "Payment Date"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:82
+msgid "Amount Authorized"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:94
+msgid "Payment Access Code"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1003
+#: waeup/sirp/students/browser.py:1225
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1072
+msgid "This ticket has already been paid."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1085
+#: waeup/sirp/students/browser.py:1093
+#: waeup/sirp/students/browser.py:1101
+#. Default: ""
+msgid "Valid callback received. ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1155
+#. Default: ""
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1167
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:117
+#: waeup/sirp/students/viewlets.py:114
+#: waeup/sirp/students/viewlets.py:534
+msgid "Study Course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1174
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1205
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1207
+msgid "Create bed ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1215
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1219
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:123
+#: waeup/sirp/students/viewlets.py:120
+#: waeup/sirp/students/viewlets.py:535
+msgid "Payments"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1230
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1241
+#: waeup/sirp/students/browser.py:1537
+#: waeup/sirp/students/browser.py:1650
+msgid "Activation code is invalid."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1261
+#: waeup/sirp/students/browser.py:1389
+#. Default: ""
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1266
+#: waeup/sirp/students/browser.py:1542
+#: waeup/sirp/students/browser.py:1655
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1269
+#: waeup/sirp/students/browser.py:1545
+#: waeup/sirp/students/browser.py:1658
+#: waeup/sirp/students/browser.py:1917
+msgid "invalidated"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1274
+#: waeup/sirp/students/browser.py:1549
+#: waeup/sirp/students/browser.py:1663
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1285
+#: waeup/sirp/students/browser.py:1403
+#. Default: ""
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1294
+#. Default: ""
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1312
+#. Default: ""
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1328
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1334
+msgid "Bed Allocation: "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1361
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1367
+msgid "Student can't be relocated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:138
+#: waeup/sirp/students/viewlets.py:126
+msgid "Accommodation"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1410
+#. Default: ""
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1429
+#. Default: ""
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1441
+#: waeup/sirp/students/viewlets.py:356
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1456
+#: waeup/sirp/students/viewlets.py:365
+msgid "Change password"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:147
+#. Default: ""
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1470
+msgid "Password changed."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1482
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1497
+#: waeup/sirp/students/viewlets.py:389
+#: waeup/sirp/students/workflow.py:44
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1501
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1518
+#: waeup/sirp/students/browser.py:1639
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1522
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1526
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1553
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1565
+msgid "Edit clearance data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1577
+#: waeup/sirp/students/browser.py:1591
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1585
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1600
+#: waeup/sirp/students/workflow.py:58
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1601
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1604
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1613
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1622
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1631
+#: waeup/sirp/students/viewlets.py:418
+msgid "Start course registration"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1635
+msgid "Start course registration now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1671
+msgid "Course registration has been started."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1690
+#. Default: ""
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1699
+msgid "Create course list now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:170
+msgid "Student Section"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1738
+#. Default: ""
+msgid "Add and remove course tickets of study level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1775
+msgid "Register course list"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1779
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1825
+msgid "Change my password"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1835
+msgid "Get new login credentials"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1849
+msgid "No student record found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1857
+msgid "You have successfully changed your password for the"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:186
+#: waeup/sirp/students/browser.py:222
+msgid "Empty search string"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1862
+#. Default: ""
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1873
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1876
+msgid "Set"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1891
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1899
+msgid "Access code is invalid."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1902
+#. Default: ""
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1908
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1914
+msgid "Access code has already been used."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1923
+#. Default: ""
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:204
+#: waeup/sirp/students/viewlets.py:139
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:228
+#: waeup/sirp/students/browser.py:1888
+msgid "No student found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:255
+#: waeup/sirp/students/viewlets.py:146
+msgid "Add student"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:263
+msgid "Student record created."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:279
+#. Default: ""
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:322
+#: waeup/sirp/students/browser.py:1865
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:333
+msgid "Manage base data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:402
+#. Default: ""
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:418
+#: waeup/sirp/students/viewlets.py:104
+#: waeup/sirp/students/viewlets.py:532
+#: waeup/sirp/students/browser_templates/clearanceeditpage.pt:6
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:424
+msgid "Clearance Slip of "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:444
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:473
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:475
+#: waeup/sirp/students/browser.py:499
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:492
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:496
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:522
+#. Default: ""
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:532
+msgid "Manage personal data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:552
+#. Default: ""
+msgid "${a}: Study Course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:582
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:585
+#: waeup/sirp/students/browser.py:626
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:586
+#: waeup/sirp/students/browser.py:612
+msgid "Add study level"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:620
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:622
+#: waeup/sirp/students/browser.py:1708
+msgid "This level exists."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:632
+msgid "No study level selected."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:668
+#. Default: ""
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:691
+msgid "Level Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:697
+msgid "Course List"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:706
+msgid "Course Registration Slip  "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:712
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:35
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:12
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:33
+msgid "Sem."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:715
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:38
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:36
+msgid "Dept."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:716
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:16
+msgid "Faculty"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:717
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:40
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:38
+msgid "Cred."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:718
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:39
+msgid "Mand."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:719
+#: waeup/sirp/students/interfaces.py:370
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:40
+msgid "Score"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:747
+#: waeup/sirp/students/browser.py:776
+#: waeup/sirp/students/browser.py:861
+#: waeup/sirp/students/browser.py:866
+#: waeup/sirp/students/browser.py:1748
+#: waeup/sirp/students/browser.py:1798
+msgid "Add course ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:768
+#. Default: ""
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:786
+#: waeup/sirp/students/browser.py:1758
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:810
+#: waeup/sirp/students/browser.py:832
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:814
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:816
+#: waeup/sirp/students/browser.py:844
+msgid "Student is in the wrong state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:837
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:841
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:881
+#: waeup/sirp/students/browser.py:1811
+msgid "The ticket exists."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:883
+#: waeup/sirp/students/browser.py:1813
+#. Default: ""
+msgid "Successfully added ${a}."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:904
+#. Default: ""
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:920
+#. Default: ""
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:947
+#. Default: ""
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:992
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:995
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:11
+msgid "Bed Category"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:29
+msgid "Registration Transition:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/changepw.pt:8
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/clearanceeditpage.pt:8
+msgid "Scans"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:28
+msgid "Select all"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:10
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:10
+msgid "with registration number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:11
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:11
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:12
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:12
+msgid "in session"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:13
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:13
+msgid "in department"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:14
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:14
+msgid "in study mode"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:30
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:36
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:31
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:37
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:32
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:38
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:14
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:25
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:87
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:54
+msgid "State"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:33
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:39
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:44
+msgid "Full Name"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:5
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:5
+msgid "for students"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:8
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:8
+msgid "with id"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:9
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:9
+msgid "with name"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:31
+msgid "Semester:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:39
+msgid "Credits:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:47
+msgid "Passmark:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/imageupload.pt:20
+#: waeup/sirp/students/browser_templates/fileupload.pt:27
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:25
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:11
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:85
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:52
+msgid "Category"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:12
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:86
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:53
+msgid "Item"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:8
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:82
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:49
+msgid "Payment Id"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:83
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:50
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/setpassword.pt:14
+msgid "Access Code:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/setpassword.pt:32
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/sirp/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/sirp/students/browser_templates/setpassword.pt:5
+msgid "Registration Number:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:6
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:9
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:52
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:30
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:57
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:35
+msgid "Level Code"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:58
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:36
+msgid "Level Title"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:71
+msgid "There are no levels registered yet."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveladdpage.pt:4
+msgid "Text to explain course registration procedure."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:15
+msgid "Department"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:6
+msgid "Course Tickets (Total Credits: ${DYNAMIC_CONTENT})"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:39
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:37
+msgid "Fact."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:43
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:19
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:41
+msgid "CO"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:44
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:42
+msgid "Auto"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:6
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:9
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:30
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:28
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:18
+msgid "Total Credits:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:64
+msgid "The're no tickets added yet."
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:176
+#: waeup/sirp/students/interfaces.py:255
+#: waeup/sirp/students/interfaces.py:483
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:183
+#: waeup/sirp/students/interfaces.py:265
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:191
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:198
+msgid "Email"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:218
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:223
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:231
+msgid "Nationality"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:241
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:275
+#: waeup/sirp/browser/templates/showstudentspage.pt:7
+msgid "Certificate"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:282
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:290
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:299
+#: waeup/sirp/browser/templates/showstudentspage.pt:8
+msgid "Current Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:307
+#: waeup/sirp/browser/templates/showstudentspage.pt:9
+msgid "Current Level"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:315
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:322
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:337
+#: waeup/sirp/students/interfaces.py:434
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:8
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:13
+msgid "Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:344
+msgid "Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:363
+#: waeup/sirp/browser/templates/certificatepage.pt:30
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:33
+msgid "Mandatory"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:377
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:384
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:403
+#: waeup/sirp/university/interfaces.py:299
+#: waeup/sirp/browser/templates/certificatepage.pt:29
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:32
+msgid "Course"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:420
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:10
+msgid "Bed Coordinates"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:442
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:448
+msgid "Booking Activation Code"
+msgstr ""
+
+#: waeup/sirp/students/utils.py:163
+#. Default: ""
+msgid "<font size=10>${a} Page ${b} of ${c}</font>"
+msgstr ""
+
+#: waeup/sirp/students/utils.py:276
+#: waeup/sirp/students/viewlets.py:71
+#: waeup/sirp/students/viewlets.py:99
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:6
+msgid "Base Data"
+msgstr ""
+
+#: waeup/sirp/students/utils.py:307
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:109
+#: waeup/sirp/students/viewlets.py:533
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:131
+#: waeup/sirp/students/viewlets.py:537
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:27
+msgid "History"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:179
+#: waeup/sirp/students/workflow.py:72
+msgid "Clear student"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:194
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:210
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:235
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:251
+#: waeup/sirp/students/workflow.py:128
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:267
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:330
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:339
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:348
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:374
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:432
+msgid "Add course list"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:450
+msgid "Add and remove courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:536
+msgid "Accommodation Data"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:548
+#. Default: ""
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:561
+msgid "File name has no extension."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:564
+msgid "File name contains more than one dot."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:571
+#. Default: ""
+msgid "${a} file extension expected."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:577
+#. Default: ""
+msgid "Only the following extension are allowed: ${a}"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:583
+msgid "Uploaded file is too big."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:590
+#. Default: ""
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:608
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:10
+#: waeup/sirp/browser/templates/datacenterpage.pt:14
+#: waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt:11
+msgid "File"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:609
+msgid "Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:632
+msgid "Upload new file"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:633
+msgid "Delete attachment"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:663
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:677
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:8
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:688
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:703
+#: waeup/sirp/students/viewlets.py:714
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:704
+#: waeup/sirp/students/viewlets.py:715
+msgid "Birth Certificate Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:724
+#: waeup/sirp/students/viewlets.py:735
+msgid "Acceptance Letter"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:725
+#: waeup/sirp/students/viewlets.py:736
+msgid "Acceptance Letter Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:85
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:135
+msgid "Male"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:137
+msgid "Female"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:77
+#. Default: ""
+msgid "Error: level id ${value} out of range"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:88
+#. Default: ""
+msgid "${title} 2nd spillover"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:91
+#. Default: ""
+msgid "${title} 3rd spillover"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:94
+#. Default: ""
+msgid "${title} 1st spillover"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:96
+#. Default: ""
+msgid "${title} on 1st probation"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:98
+#. Default: ""
+msgid "${title} on 2nd probation"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:107
+msgid "Reset to returning"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:108
+msgid "Student record reset to 'returning'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:114
+msgid "Register courses"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:115
+msgid "Courses registered"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:121
+#: waeup/sirp/students/workflow.py:135
+msgid "Reset to paid"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:122
+#: waeup/sirp/students/workflow.py:136
+msgid "Student record reset to 'paid'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:129
+msgid "Courses validated"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:142
+msgid "Return"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:143
+msgid "Returned"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:149
+msgid "Reset to validated"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:150
+msgid "Student record reset to 'validated'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:22
+msgid "Create student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:30
+msgid "Admit student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:31
+msgid "Student admitted"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:37
+msgid "Reset student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:38
+msgid "Student record reset"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:45
+msgid "Clearance started"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:51
+msgid "Reset to admitted"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:52
+msgid "Student record reset to 'admitted'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:59
+msgid "Clearance requested"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:65
+#: waeup/sirp/students/workflow.py:79
+msgid "Reset to clearance"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:66
+#: waeup/sirp/students/workflow.py:80
+msgid "Student record reset to 'clearance'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:73
+msgid "Cleared"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:86
+#: waeup/sirp/students/workflow.py:100
+msgid "Pay school fee"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:87
+#: waeup/sirp/students/workflow.py:101
+msgid "School fee paid"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:93
+msgid "Reset to cleared"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:94
+msgid "Student record reset to 'cleared'"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:146
+msgid "Title of course"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:152
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:17
+msgid "Credits"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:158
+msgid "Passmark"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:164
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:180
+msgid "Enter unique course code which will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:205
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:212
+msgid "Start Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:219
+msgid "End Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:226
+msgid "Application Category"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:243
+msgid "Enter unique certificate code which will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:264
+msgid "Course Referrer"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:270
+#: waeup/sirp/browser/templates/certificatepage.pt:28
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:30
+msgid "Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:277
+msgid "Is mandatory course (not elective)"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:45
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:46
+#: waeup/sirp/university/interfaces.py:94
+#: waeup/sirp/university/interfaces.py:147
+#: waeup/sirp/university/interfaces.py:200
+msgid "Unnamed"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:51
+#: waeup/sirp/university/interfaces.py:99
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:67
+#: waeup/sirp/university/interfaces.py:118
+msgid "This code will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:93
+msgid "Name of department"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:28
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:29
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:30
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:31
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:32
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:33
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:34
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:35
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:36
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/sirp/utils/utils.py:119
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/sirp/utils/utils.py:164
+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"
+"Login page: ${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 ""
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/yo/LC_MESSAGES/waeup.sirp.po
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/yo/LC_MESSAGES/waeup.sirp.po	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/locales/yo/LC_MESSAGES/waeup.sirp.po	(revision 7760)
@@ -0,0 +1,3570 @@
+# #############################################################################
+#
+# 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.SIRP\n"
+"POT-Creation-Date: Fri Mar  2 12:58:47 2012\n"
+"PO-Revision-Date: 2012-02-22 11:28+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/sirp/accesscodes/browser.py:100 waeup/sirp/accesscodes/browser.py:113
+#: waeup/sirp/applicants/browser.py:80 waeup/sirp/applicants/browser.py:130
+#: waeup/sirp/applicants/browser.py:182 waeup/sirp/applicants/browser.py:254
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:338
+#: waeup/sirp/browser/pages.py:503 waeup/sirp/browser/pages.py:708
+#: waeup/sirp/browser/pages.py:735 waeup/sirp/browser/pages.py:758
+#: waeup/sirp/browser/pages.py:805 waeup/sirp/browser/pages.py:835
+#: waeup/sirp/browser/pages.py:955 waeup/sirp/browser/pages.py:1183
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1298
+#: waeup/sirp/browser/pages.py:1325 waeup/sirp/browser/pages.py:1353
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1400
+#: waeup/sirp/browser/pages.py:1441 waeup/sirp/browser/pages.py:1520
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1523
+#: waeup/sirp/browser/pages.py:1601 waeup/sirp/browser/pages.py:1646
+#: waeup/sirp/browser/pages.py:1684 waeup/sirp/browser/pages.py:1728
+#: waeup/sirp/browser/pages.py:1767 waeup/sirp/browser/pages.py:1769
+#: waeup/sirp/browser/pages.py:1804 waeup/sirp/browser/pages.py:1855
+#: waeup/sirp/browser/pages.py:1907 waeup/sirp/students/browser.py:584
+#: waeup/sirp/students/browser.py:585 waeup/sirp/students/browser.py:746
+#: waeup/sirp/students/browser.py:748 waeup/sirp/students/browser.py:888
+msgid "Cancel"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:102
+msgid "Batch creation cancelled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:112
+msgid "Reimport"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:115
+msgid "Reimport Access Code Batches"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:120
+msgid "Reimport cancelled."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:126
+msgid "No file chosen. Action cancelled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:136
+msgid "This batch already exists: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:139
+msgid "Successfully reimported: ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:152
+msgid "Search and Manage Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:153 waeup/sirp/browser/pages.py:592
+#: waeup/sirp/browser/pages.py:1142 waeup/sirp/browser/viewlets.py:190
+#: waeup/sirp/students/browser.py:171 waeup/sirp/students/browser.py:205
+msgid "Search"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:154
+msgid "Disable ACs"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:155
+msgid "Enable ACs"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:180 waeup/sirp/accesscodes/workflow.py:39
+msgid "disabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:182
+msgid "${a} disabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:186
+msgid "${a}: Disable transition not allowed."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:190
+msgid "re-enabled"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:192
+msgid "${a} (re-)enabled."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:196
+msgid "${a}: Re-enable transition not allowed."
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:235
+msgid "Add Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:244
+msgid "Reimport Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:252
+msgid "Search Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:261
+msgid "Access Codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:43
+msgid "Archive"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:44
+msgid "Archive and delete"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:46 waeup/sirp/accesscodes/browser.py:206
+#: waeup/sirp/accesscodes/browser.py:226
+msgid "Access Code Batches"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:53
+msgid "No batch selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:61
+msgid "Archived ${a} (${b})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:68
+msgid "Deleted batch ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:77
+msgid "Create Access Code Batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser.py:83
+msgid "Create batch"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:91
+msgid "Batch created (${a} entries)"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/accesscodes/browser.py:93
+msgid "Data written to ${a}"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:10
+msgid "Entries/(invalidated)"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:11
+msgid "Cost"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:12
+msgid "Date of Creation"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:13
+msgid "Creator"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:2
+msgid "The following batches are available:"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:39
+msgid "No batches yet"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/batchcontainer.pt:9
+msgid "Prefix"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt:23
+msgid "No import batches available"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:23
+msgid "Serial"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:24
+msgid "AC"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:26
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:38
+msgid "Owner"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:5
+msgid "for access codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:7
+msgid "with PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:8
+msgid "with serial"
+msgstr ""
+
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:9
+msgid "with message term"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:29
+msgid "Serial number inside batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:32
+msgid "Prefix inside batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:35
+msgid "Batch number"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:38
+msgid "Random part of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:41
+#: waeup/sirp/accesscodes/interfaces.py:80
+msgid "Cost of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:45
+msgid "Workflow state"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:48
+msgid "Complete title of access code"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:51
+msgid "Purchaser"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:54
+msgid "The history of access code as lines"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:63
+msgid "Creation date"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:66
+msgid "Batch creator"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:69
+msgid "Batch prefix"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:72
+msgid "Batch number (1-3 digits)"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:76
+msgid "Number of access codes"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:84
+msgid "Number of disabled access codes inside the batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/interfaces.py:89
+msgid "Number of used access codes inside the batch"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:102
+msgid "Disable used PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:109
+msgid "Reenable disabled PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:37 waeup/sirp/applicants/workflow.py:37
+msgid "initialized"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:38
+msgid "used"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:81
+msgid "Initialize PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:88
+msgid "Use PIN"
+msgstr ""
+
+#: waeup/sirp/accesscodes/workflow.py:95
+msgid "Disable unused PIN"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:117 waeup/sirp/applicants/browser.py:325
+#: waeup/sirp/applicants/browser.py:745
+msgid "Could not delete:"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:120 waeup/sirp/applicants/browser.py:328
+#: waeup/sirp/applicants/browser.py:748 waeup/sirp/browser/pages.py:134
+#: waeup/sirp/students/browser.py:243 waeup/sirp/students/browser.py:642
+#: waeup/sirp/students/browser.py:796 waeup/sirp/students/browser.py:974
+#: waeup/sirp/students/browser.py:1184 waeup/sirp/students/browser.py:1770
+msgid "Successfully removed: ${a}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:169
+msgid ""
+"An applicants container for the same application type and entrance year "
+"exists already in the database."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:178
+msgid "Added:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:190 waeup/sirp/applicants/viewlets.py:44
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:12
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:36
+msgid "Applicants"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:254 waeup/sirp/applicants/browser.py:301
+#: waeup/sirp/applicants/browser.py:654 waeup/sirp/applicants/browser.py:688
+#: waeup/sirp/applicants/browser.py:791 waeup/sirp/applicants/browser.py:794
+#: waeup/sirp/applicants/browser.py:838 waeup/sirp/browser/pages.py:476
+#: waeup/sirp/browser/pages.py:631 waeup/sirp/browser/pages.py:654
+#: waeup/sirp/browser/pages.py:728 waeup/sirp/browser/pages.py:1181
+#: waeup/sirp/browser/pages.py:1353 waeup/sirp/browser/pages.py:1394
+#: waeup/sirp/browser/pages.py:1520 waeup/sirp/browser/pages.py:1573
+#: waeup/sirp/browser/pages.py:1716 waeup/sirp/browser/pages.py:1767
+#: waeup/sirp/browser/pages.py:1788 waeup/sirp/hostels/browser.py:209
+#: waeup/sirp/hostels/browser.py:231 waeup/sirp/hostels/browser.py:319
+#: waeup/sirp/students/browser.py:359 waeup/sirp/students/browser.py:458
+#: waeup/sirp/students/browser.py:535 waeup/sirp/students/browser.py:584
+#: waeup/sirp/students/browser.py:600 waeup/sirp/students/browser.py:746
+#: waeup/sirp/students/browser.py:771 waeup/sirp/students/browser.py:1444
+#: waeup/sirp/students/browser.py:1459 waeup/sirp/students/browser.py:1574
+msgid "Save"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:333
+#: waeup/sirp/applicants/browser.py:361
+msgid "Add applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:265 waeup/sirp/applicants/viewlets.py:95
+msgid "Manage applicants container"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:305 waeup/sirp/applicants/browser.py:717
+#: waeup/sirp/browser/pages.py:1397 waeup/sirp/browser/pages.py:1719
+#: waeup/sirp/browser/pages.py:1791 waeup/sirp/browser/pages.py:1903
+#: waeup/sirp/hostels/browser.py:54 waeup/sirp/students/browser.py:80
+#: waeup/sirp/students/browser.py:385
+msgid "Form has been saved."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:314
+msgid "No applicant selected!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:364
+msgid "Create application record"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:369
+msgid "Applicant record created."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:382
+msgid "Applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:397 waeup/sirp/students/browser.py:285
+msgid "set"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:398 waeup/sirp/students/browser.py:286
+msgid "unset"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:403
+msgid "${a} Application Record ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:454
+msgid "Session configuration object is not available."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:461 waeup/sirp/students/browser.py:1023
+msgid "This type of payment has already been made."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:474 waeup/sirp/students/browser.py:1037
+msgid "Payment ticket created."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:501 waeup/sirp/students/browser.py:1056
+msgid "${a}: Online Payment Ticket ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:529 waeup/sirp/students/browser.py:1105
+msgid "Valid callback received."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:550
+msgid "Payment Data"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:556
+msgid "Online Payment Receipt"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:562
+msgid "Ticket not yet paid."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:627
+msgid "Uploaded image is too big!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:632
+msgid "jpg file extension expected."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:64
+msgid "Application Section"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:654 waeup/sirp/applicants/browser.py:794
+#: waeup/sirp/applicants/browser.py:846
+msgid "Final Submit"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:655 waeup/sirp/applicants/browser.py:758
+#: waeup/sirp/applicants/browser.py:792 waeup/sirp/applicants/browser.py:827
+#: waeup/sirp/students/browser.py:980
+msgid "Add online payment ticket"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:655 waeup/sirp/applicants/browser.py:763
+#: waeup/sirp/applicants/browser.py:792 waeup/sirp/applicants/browser.py:795
+#: waeup/sirp/applicants/browser.py:832 waeup/sirp/students/browser.py:748
+#: waeup/sirp/students/browser.py:780 waeup/sirp/students/browser.py:956
+#: waeup/sirp/students/browser.py:1752
+msgid "Remove selected tickets"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:674
+msgid "${a} Application Form ${b}"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:685 waeup/sirp/students/browser.py:356
+msgid "No transition"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:732 waeup/sirp/students/browser.py:962
+msgid "No payment selected."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:76 waeup/sirp/applicants/viewlets.py:88
+msgid "Manage application section"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:79 waeup/sirp/applicants/browser.py:105
+#: waeup/sirp/applicants/browser.py:255 waeup/sirp/applicants/browser.py:308
+#: waeup/sirp/browser/pages.py:634 waeup/sirp/browser/pages.py:675
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1288
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1389
+#: waeup/sirp/hostels/browser.py:131 waeup/sirp/students/browser.py:206
+#: waeup/sirp/students/browser.py:1151 waeup/sirp/students/browser.py:1164
+msgid "Remove selected"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:79 waeup/sirp/applicants/browser.py:125
+#: waeup/sirp/applicants/browser.py:148 waeup/sirp/applicants/browser.py:160
+msgid "Add applicants container"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:805 waeup/sirp/students/browser.py:88
+msgid "The requested form is locked (read-only)."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:81 waeup/sirp/applicants/browser.py:139
+#: waeup/sirp/applicants/browser.py:256 waeup/sirp/applicants/browser.py:347
+#: waeup/sirp/browser/pages.py:1355 waeup/sirp/browser/pages.py:1414
+#: waeup/sirp/browser/pages.py:1524 waeup/sirp/browser/pages.py:1610
+#: waeup/sirp/browser/pages.py:1770 waeup/sirp/browser/pages.py:1827
+msgid "Remove selected local roles"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:819
+msgid "No passport picture uploaded."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:82 waeup/sirp/applicants/browser.py:135
+#: waeup/sirp/applicants/browser.py:257 waeup/sirp/applicants/browser.py:343
+#: waeup/sirp/browser/pages.py:1356 waeup/sirp/browser/pages.py:1410
+#: waeup/sirp/browser/pages.py:1525 waeup/sirp/browser/pages.py:1606
+#: waeup/sirp/browser/pages.py:1771 waeup/sirp/browser/pages.py:1823
+msgid "Add local role"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:821
+msgid "Passport picture confirmation box not ticked."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:83
+msgid "Applicants Containers"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:858 waeup/sirp/students/browser.py:1619
+msgid "This form cannot be submitted. Wrong state!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:863
+msgid "Form has been submitted."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/browser.py:898
+msgid "Register for ${a} Application"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:904
+msgid "Application has not yet started."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:909
+msgid "Application has ended."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:918
+msgid "Get login credentials"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:933
+msgid "You have successfully been registered for the"
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:940
+msgid "Email could not been sent. Please retry later."
+msgstr ""
+
+#: waeup/sirp/applicants/browser.py:949
+msgid "Your registration was successful."
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:18
+msgid "Photograph"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:43
+msgid "Application Transition:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:59
+msgid ""
+"I confirm that the Passport Photograph uploaded on this form is a true "
+"picture of me."
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:74
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:43
+msgid "Acceptance Fee Payment Tickets"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:11
+msgid "Thanks for using SIRP!"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:3
+msgid "An email with your user name and password has been sent to"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantregemailsent.pt:8
+msgid "Please check your email account to proceed."
+msgstr ""
+
+#: waeup/sirp/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/sirp/applicants/browser_templates/applicantregister.pt:8
+msgid "Please solve the captcha<br />to prevent misuse of this service:"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:42
+msgid "Application Number"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:46
+msgid "First Choice"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:48
+msgid "Application State"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:4
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:23
+msgid "Year"
+msgstr ""
+
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:7
+msgid "Application Period"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:197 waeup/sirp/students/browser.py:713
+#: waeup/sirp/university/interfaces.py:38
+#: waeup/sirp/university/interfaces.py:66
+#: waeup/sirp/university/interfaces.py:86
+#: waeup/sirp/university/interfaces.py:117
+#: waeup/sirp/university/interfaces.py:139
+#: waeup/sirp/university/interfaces.py:179
+#: waeup/sirp/university/interfaces.py:192
+#: waeup/sirp/university/interfaces.py:241
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:39
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:68
+#: waeup/sirp/browser/templates/facultypage.pt:8
+#: waeup/sirp/browser/templates/departmentpage.pt:15
+#: waeup/sirp/browser/templates/departmentpage.pt:39
+#: waeup/sirp/browser/templates/searchpage.pt:13
+#: waeup/sirp/browser/templates/facultymanagepage.pt:41
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:36
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:13
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:34
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:5
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:25
+msgid "Code"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:204 waeup/sirp/students/browser.py:714
+#: waeup/sirp/university/interfaces.py:199
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:41
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:70
+#: waeup/sirp/browser/templates/facultypage.pt:9
+#: waeup/sirp/browser/templates/departmentpage.pt:16
+#: waeup/sirp/browser/templates/departmentpage.pt:40
+#: waeup/sirp/browser/templates/searchpage.pt:14
+#: waeup/sirp/browser/templates/certificatepage.pt:30
+#: waeup/sirp/browser/templates/facultymanagepage.pt:43
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:32
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:37
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:14
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:35
+#: waeup/sirp/applicants/browser_templates/applicantsrootpage.pt:6
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:27
+msgid "Title"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:211
+#: waeup/sirp/applicants/interfaces.py:324
+msgid "Application Target"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:219
+msgid "Entry Level"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:226
+#: waeup/sirp/applicants/interfaces.py:332
+msgid "Year of Entrance"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:234
+#: waeup/sirp/applicants/interfaces.py:340
+msgid "Applicants Container Type"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:250
+msgid "Category for the grouping of certificates"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:257
+msgid "Human readable description in reST format"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:273
+msgid "Application Start Date"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:279
+msgid "Application Closing Date"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:285
+msgid "Forbid additions after deadline (enddate)"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:368
+msgid "Applicant Id"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:373
+msgid "JAMB Registration Number"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:385
+#: waeup/sirp/students/interfaces.py:154
+msgid "First Name"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:389
+#: waeup/sirp/students/interfaces.py:159
+msgid "Middle Name"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:393
+#: waeup/sirp/students/interfaces.py:164
+msgid "Last Name (Surname)"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:397
+#: waeup/sirp/students/interfaces.py:213
+msgid "Date of Birth"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:402
+msgid "State/LGA"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:407
+#: waeup/sirp/students/interfaces.py:169
+msgid "Sex"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:413
+#: waeup/sirp/students/interfaces.py:490
+msgid "Email Address"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:418
+#: waeup/sirp/students/interfaces.py:203
+msgid "Phone"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:423
+#: waeup/sirp/applicants/interfaces.py:503
+msgid "1st Choice Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:428
+#: waeup/sirp/applicants/interfaces.py:508
+msgid "2nd Choice Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:437
+#: waeup/sirp/applicants/interfaces.py:513
+msgid "Screening Score"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:441
+#: waeup/sirp/applicants/interfaces.py:518
+msgid "Screening Venue"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:445
+#: waeup/sirp/applicants/interfaces.py:523
+msgid "Admitted Course of Study"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:451
+#: waeup/sirp/applicants/interfaces.py:530
+msgid "Notice"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:473
+#: waeup/sirp/students/interfaces.py:149
+#: waeup/sirp/browser/templates/showstudentspage.pt:4
+#: waeup/sirp/students/browser_templates/containerpage.pt:29
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:35
+msgid "Student Id"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:478
+msgid "Form locked"
+msgstr ""
+
+#: waeup/sirp/applicants/interfaces.py:557
+#: waeup/sirp/students/interfaces.py:469
+msgid "Payment Session"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:110
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:23
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:28
+msgid "Department:"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:121
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:15
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:39
+msgid "Faculty:"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:182
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:26
+msgid "Admitted Course of Study:"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/applicants/pdf.py:200
+msgid ""
+"<font size=10>Proceed to the login page of the portal and enter your new "
+"credentials: user name= ${a}, password = ${b}.</font>"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:206
+msgid "<font size=10>Change your password when you have  logged in.</font>"
+msgstr ""
+
+#: waeup/sirp/applicants/pdf.py:57
+msgid "Application Record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:103
+msgid "Register for application"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:111
+msgid "View application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:119
+msgid "Manage application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:127
+msgid "Edit application record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:143
+msgid "Download application slip"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:151 waeup/sirp/applicants/workflow.py:99
+#: waeup/sirp/students/browser.py:258
+msgid "Create student record"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:168 waeup/sirp/students/viewlets.py:300
+msgid "Download payment receipt"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:183 waeup/sirp/students/viewlets.py:315
+msgid "Request callback"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:57
+msgid "Application"
+msgstr ""
+
+#: waeup/sirp/applicants/viewlets.py:73 waeup/sirp/students/viewlets.py:512
+msgid "My Data"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:100 waeup/sirp/students/workflow.py:25
+msgid "Student record created"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:106 waeup/sirp/applicants/workflow.py:113
+#: waeup/sirp/applicants/workflow.py:120 waeup/sirp/applicants/workflow.py:127
+msgid "Reset application to started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:107 waeup/sirp/applicants/workflow.py:114
+#: waeup/sirp/applicants/workflow.py:121 waeup/sirp/applicants/workflow.py:128
+#: waeup/sirp/applicants/workflow.py:134
+msgid "Application reset"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:133
+msgid "Reset application to paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:38
+msgid "started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:39
+msgid "paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:40
+msgid "submitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:41 waeup/sirp/interfaces.py:71
+msgid "admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:42
+msgid "not admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:43 waeup/sirp/interfaces.py:70
+msgid "created"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:49
+msgid "Initialize application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:52
+msgid "Application initialized"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:57
+msgid "Start application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:58
+msgid "Application started"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:64
+msgid "Pay acceptance fee"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:65
+msgid "Fee paid"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:71
+msgid "Submit application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:72
+msgid "Application submitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:78
+msgid "Admit applicant"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:79
+msgid "Applicant admitted"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:85 waeup/sirp/applicants/workflow.py:92
+msgid "Refuse application"
+msgstr ""
+
+#: waeup/sirp/applicants/workflow.py:86 waeup/sirp/applicants/workflow.py:93
+msgid "Application refused"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:104
+msgid "Portal Settings"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:116 waeup/sirp/browser/viewlets.py:615
+msgid "Academics"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:123 waeup/sirp/browser/pages.py:309
+msgid "Administration"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:130 waeup/sirp/browser/viewlets.py:360
+#: waeup/sirp/browser/viewlets.py:666
+msgid "Portal Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:148 waeup/sirp/browser/pages.py:386
+#: waeup/sirp/browser/viewlets.py:375 waeup/sirp/browser/viewlets.py:676
+msgid "Portal Users"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:155 waeup/sirp/browser/pages.py:748
+#: waeup/sirp/browser/pages.py:1140 waeup/sirp/browser/viewlets.py:393
+#: waeup/sirp/browser/viewlets.py:685
+msgid "Data Center"
+msgstr ""
+
+#: waeup/sirp/browser/breadcrumbs.py:90
+msgid "Home"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1016
+msgid "Edit headers or replace imported file!"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:103
+msgid "Local role successfully removed: ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1063
+msgid "View processing log"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1064
+msgid "Back to data center"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1108
+msgid "Processing of ${a} rows failed."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1110
+msgid "Successfully processed ${a} rows."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1118
+msgid "Show logfiles"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1120
+msgid "Back to Data Center"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1121
+msgid "Show"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:113 waeup/sirp/hostels/browser.py:256
+#: waeup/sirp/hostels/browser.py:287 waeup/sirp/students/browser.py:230
+msgid "No item selected."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1143
+msgid "Back"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1151
+msgid "Log files can only be searched on Unix-based operating systems."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1179
+msgid "Edit data center settings"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1199
+msgid "File already existed (not copied): ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1202
+msgid "Given storage path cannot be used."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1203
+msgid "Error: ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1206
+msgid "New storage path succefully set."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1265
+msgid "Academic Section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1277 waeup/sirp/browser/pages.py:1293
+#: waeup/sirp/browser/pages.py:1310 waeup/sirp/browser/pages.py:1314
+msgid "Add faculty"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1278
+msgid "Faculties"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1282 waeup/sirp/browser/viewlets.py:441
+msgid "Manage academic section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1321
+msgid "The faculty code chosen already exists."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1342 waeup/sirp/browser/pages.py:1352
+msgid "Departments"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1354 waeup/sirp/browser/pages.py:1405
+#: waeup/sirp/browser/pages.py:1424 waeup/sirp/browser/pages.py:1428
+msgid "Add department"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1362 waeup/sirp/browser/viewlets.py:457
+msgid "Manage faculty"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1435
+msgid "The code chosen already exists in this faculty."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1438
+msgid "Department ${a} added."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1455
+msgid "Courses and Certificates"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1485 waeup/sirp/browser/pages.py:1504
+#: waeup/sirp/students/browser.py:96 waeup/sirp/students/viewlets.py:472
+msgid "Students"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:152 waeup/sirp/browser/viewlets.py:322
+#: waeup/sirp/browser/templates/staffsitelayout.pt:28
+#: waeup/sirp/browser/templates/studentsitelayout.pt:33
+msgid "Login"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1579
+msgid "Remove selected courses"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1521 waeup/sirp/browser/pages.py:1591
+#: waeup/sirp/browser/pages.py:1620 waeup/sirp/browser/pages.py:1624
+msgid "Add course"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1522 waeup/sirp/browser/pages.py:1585
+msgid "Remove selected certificates"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1522 waeup/sirp/browser/pages.py:1596
+#: waeup/sirp/browser/pages.py:1657 waeup/sirp/browser/pages.py:1661
+msgid "Add certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1531 waeup/sirp/browser/viewlets.py:464
+msgid "Manage department"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:160
+msgid "You logged in."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1636
+msgid "A course with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1642
+msgid "Course ${a} successfully created."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1673
+msgid "A certificate with same code already exists: "
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1679
+msgid "Certificate ${a} successfully created."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1711 waeup/sirp/browser/viewlets.py:481
+msgid "Edit course"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1761
+msgid "Edit certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1768 waeup/sirp/browser/pages.py:1799
+#: waeup/sirp/browser/pages.py:1840 waeup/sirp/browser/pages.py:1842
+msgid "Add course referrer"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1769 waeup/sirp/browser/pages.py:1794
+msgid "Remove selected course referrers"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1775 waeup/sirp/browser/viewlets.py:488
+msgid "Manage certificate"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:178
+msgid "You entered wrong credentials."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1847
+msgid "The chosen course referrer is already part of this certificate."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:1851
+msgid "Course referrer ${a}_${b} added."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1888 waeup/sirp/browser/viewlets.py:505
+msgid "Edit course referrer"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:1891
+msgid "Save and return"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:192
+msgid "You have been logged out. Thanks for using WAeUP SIRP!"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:228
+msgid "Contact ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:234 waeup/sirp/browser/pages.py:519
+msgid "Send message now"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:253 waeup/sirp/browser/pages.py:272
+#: waeup/sirp/browser/pages.py:536 waeup/sirp/students/browser.py:320
+msgid "Your message has been sent."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:263
+msgid "Send now"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:274
+msgid "A smtp server error occurred."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:299
+msgid "<h1>Welcome to WAeUP.SIRP</h1>"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:387 waeup/sirp/hostels/browser.py:195
+#: waeup/sirp/students/viewlets.py:163 waeup/sirp/students/viewlets.py:171
+#: waeup/sirp/students/viewlets.py:226 waeup/sirp/students/viewlets.py:243
+#: waeup/sirp/students/viewlets.py:283
+msgid "Manage"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:388 waeup/sirp/browser/viewlets.py:180
+msgid "Remove"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:395
+msgid "User account ${a} successfully deleted."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:426 waeup/sirp/browser/pages.py:428
+#: waeup/sirp/browser/viewlets.py:515
+msgid "Add user"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:450
+msgid "User account ${a} successfully added."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:469
+msgid "Edit user ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:500
+msgid "User settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:517 waeup/sirp/students/browser.py:301
+msgid "Send message to ${a}"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:546 waeup/sirp/browser/viewlets.py:400
+msgid "My Preferences"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:559 waeup/sirp/browser/viewlets.py:415
+msgid "My Roles"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:590
+msgid "Search Academic Section"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:617
+msgid "View portal configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:630
+msgid "Edit portal configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:631 waeup/sirp/browser/pages.py:680
+msgid "Update plugins"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:633 waeup/sirp/browser/pages.py:661
+#: waeup/sirp/browser/pages.py:692
+msgid "Add session configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:658 waeup/sirp/browser/pages.py:731
+msgid "Settings have been saved."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:672
+msgid "Session ${a} Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:683
+msgid "Plugins were updated. See log file for details."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:696
+msgid "Add Session Configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:703
+msgid "The session chosen already exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/browser/pages.py:725
+msgid "Edit academic session ${a} configuration"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:755
+msgid "Upload file"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:757
+msgid "Upload"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:803 waeup/sirp/browser/pages.py:833
+#: waeup/sirp/browser/pages.py:953 waeup/sirp/browser/pages.py:1061
+msgid "Process CSV file"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:817 waeup/sirp/browser/pages.py:918
+#: waeup/sirp/browser/pages.py:1037
+msgid "Import aborted."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:83
+msgid "No local role selected."
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:836
+msgid "Back to step 1"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:837
+msgid "Proceed to step 3"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:897
+msgid "Double headers: each column name may only appear once. "
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:905
+msgid "Replace imported file!"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:930
+msgid "Update mode only!"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:956 waeup/sirp/browser/pages.py:1182
+msgid "Reset"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:957
+msgid "Set headerfields"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:958
+msgid "Back to step 2"
+msgstr ""
+
+#: waeup/sirp/browser/pages.py:959
+msgid "Perform import"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:10
+msgid "Course Code:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:14
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:7
+msgid "Course Title:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:18
+msgid "Provided by:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:26
+msgid "Level:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:30
+msgid "Is mandatory course (not elective):"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatecoursepage.pt:6
+msgid "Code:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:22
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:6
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:26
+msgid "Course Referrers"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:28
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:31
+msgid "Semester"
+msgstr ""
+
+#: waeup/sirp/browser/templates/certificatepage.pt:29
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:31
+msgid "Referrer"
+msgstr ""
+
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:5
+msgid "Site Settings"
+msgstr ""
+
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:8
+msgid "Session Configurations"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:1
+msgid "Step 1"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:13
+msgid "Filename"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:14
+msgid "Datasets"
+msgstr ""
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterimport1page.pt:21
+msgid "Select"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:6
+msgid "Please select a file for processing from the list below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:1
+msgid "Step 2"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:43
+msgid "Mode:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:8
+msgid ""
+"Please select a file-processor and a processing-mode from the selections "
+"below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:1
+msgid "Header fields OK"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:12
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:6
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:4
+msgid "File:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:18
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:9
+#: waeup/sirp/browser/templates/datacenterimport2page.pt:32
+msgid "Processor:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:24
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:13
+msgid "Processing mode:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:44
+msgid "change to:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:6
+msgid "Step 3"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport3page.pt:8
+msgid "Eventually modify headerfields of import file below."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:1
+msgid "Step 4"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterimport4page.pt:2
+msgid "Batch processing finished."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:12
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:16
+msgid "Date"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:14
+#: waeup/sirp/browser/templates/datacenterpage.pt:15
+#: waeup/sirp/browser/templates/datacenterimport1page.pt:15
+msgid "Size"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:2
+msgid "Currently no log files are available."
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:12
+msgid "Overwrite contents in new location?"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:3
+#: waeup/sirp/browser/templates/datacenterpage.pt:7
+msgid "Storage path:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/datacentermanagepage.pt:7
+msgid "Copy existing data to new storage?"
+msgstr ""
+
+#: waeup/sirp/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/sirp/browser/templates/datacenterpage.pt:16
+msgid "Upload Date"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:10
+#: waeup/sirp/browser/templates/departmentpage.pt:3
+msgid "Courses"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:101
+#: waeup/sirp/browser/templates/facultymanagepage.pt:75
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:92
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:93
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:60
+msgid "Local Role"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:13
+#: waeup/sirp/browser/templates/departmentpage.pt:6
+msgid "Certificates"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:16
+#: waeup/sirp/browser/templates/userscontainerpage.pt:7
+#: waeup/sirp/browser/templates/facultymanagepage.pt:16
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:7
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:17
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:11
+msgid "Local Roles"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:6
+#: waeup/sirp/browser/templates/facultymanagepage.pt:6
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:5
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:7
+msgid "Settings"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:97
+#: waeup/sirp/browser/templates/userscontainerpage.pt:4
+#: waeup/sirp/browser/templates/facultymanagepage.pt:71
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:88
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:89
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:56
+msgid "User Id"
+msgstr ""
+
+#: waeup/sirp/browser/templates/departmentmanagepage.pt:99
+#: waeup/sirp/browser/templates/userscontainerpage.pt:5
+#: waeup/sirp/browser/templates/facultymanagepage.pt:73
+#: waeup/sirp/browser/templates/configurationmanagepage.pt:31
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:90
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:91
+#: waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt:58
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:10
+#: waeup/sirp/hostels/browser_templates/containermanagepage.pt:11
+msgid "Name"
+msgstr ""
+
+#: waeup/sirp/browser/templates/facultypage.pt:1
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:1
+msgid "There no subobjects registered yet."
+msgstr ""
+
+#: waeup/sirp/browser/templates/loginpage.pt:13
+#: waeup/sirp/browser/templates/usereditformpage.pt:8
+#: waeup/sirp/students/browser_templates/basepage.pt:14
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:17
+#: waeup/sirp/students/browser_templates/change_password.pt:10
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:31
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:34
+msgid "Password:"
+msgstr ""
+
+#: waeup/sirp/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/sirp/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 Access Code (PWD) and inititialize your student account <strong><a href=\"setpassword\"> here</a></strong>."
+#: waeup/sirp/browser/templates/loginpage.pt:39
+msgid "login_trouble2"
+msgstr ""
+
+#. Default: "Or simply forgot your student id or password? Then request a new password <strong><a href=\"changepw\"> here</a></strong>."
+#: waeup/sirp/browser/templates/loginpage.pt:44
+msgid "login_trouble3"
+msgstr ""
+
+#: waeup/sirp/browser/templates/loginpage.pt:5
+msgid "User Name or Id:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/myrolespage.pt:1
+msgid "My Portal Roles:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/myrolespage.pt:11
+msgid "My Local Roles:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:1
+msgid "The page you are trying to access is not available."
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:12
+msgid "You might be trying to access a non-existing page."
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:6
+msgid "Please note the following:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/notfound.pt:9
+msgid "You might have misspelled the URL."
+msgstr ""
+
+#: waeup/sirp/browser/templates/searchpage.pt:15
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:36
+msgid "Type"
+msgstr ""
+
+#: waeup/sirp/browser/templates/searchpage.pt:9
+#: waeup/sirp/students/browser_templates/containerpage.pt:21
+msgid "Search Results"
+msgstr ""
+
+#: waeup/sirp/browser/templates/showstudentspage.pt:5
+msgid "Fullname"
+msgstr ""
+
+#: waeup/sirp/browser/templates/showstudentspage.pt:6
+msgid "Status"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:101
+#: waeup/sirp/browser/templates/studentsitelayout.pt:86
+msgid "Form Status:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:113
+#: waeup/sirp/browser/templates/studentsitelayout.pt:99
+msgid "Back to top"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:114
+#: waeup/sirp/browser/templates/studentsitelayout.pt:100
+msgid ""
+"Student Information and Registration Portal<br /> Copyright &copy; WAeUP "
+"Group, 2008-2012"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:34
+#: waeup/sirp/browser/templates/studentsitelayout.pt:39
+msgid "Contact"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:40
+#: waeup/sirp/browser/templates/studentsitelayout.pt:45
+msgid "Logout"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:60
+msgid "Student Id:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/staffsitelayout.pt:64
+msgid "State:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/usereditformpage.pt:14
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:23
+#: waeup/sirp/students/browser_templates/change_password.pt:16
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:37
+msgid "Retype password:"
+msgstr ""
+
+#: waeup/sirp/browser/templates/userscontainerpage.pt:6
+msgid "Portal Roles"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:170
+msgid "Add"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:383 waeup/sirp/students/viewlets.py:155
+msgid "Send email"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:434
+msgid "Edit settings"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:448
+msgid "Search academic section"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:473 waeup/sirp/browser/viewlets.py:497
+msgid "Show students"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:528 waeup/sirp/browser/viewlets.py:536
+msgid "Show batch logs"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:548
+msgid "Batch processing"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:557
+msgid "Upload CSV file"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:630
+msgid "Enquiries"
+msgstr ""
+
+#: waeup/sirp/browser/viewlets.py:91 waeup/sirp/browser/viewlets.py:160
+#: waeup/sirp/students/viewlets.py:218 waeup/sirp/students/viewlets.py:403
+msgid "Edit"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:105
+msgid "Accommodation Section"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:113 waeup/sirp/hostels/browser.py:123
+msgid "Manage accommodation section"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:145 waeup/sirp/hostels/browser.py:158
+msgid "Add hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:161
+msgid "Create hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:170
+msgid "The hostel already exists."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:172
+msgid "Hostel created."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:207
+msgid "Manage hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:210 waeup/sirp/hostels/browser.py:236
+msgid "Update all beds"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:211 waeup/sirp/hostels/browser.py:250
+msgid "Switch reservation of selected beds"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:212 waeup/sirp/hostels/browser.py:281
+msgid "Release selected beds"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:241
+msgid ""
+"${a} empty beds removed, ${b} beds added, ${c} occupied beds modified (${d})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:275
+msgid "Successfully switched beds: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:299
+msgid "Successfully released beds: ${a}"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:304
+msgid "No allocated bed selected."
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:316
+msgid "Allocate student"
+msgstr ""
+
+#: waeup/sirp/hostels/browser.py:68 waeup/sirp/hostels/browser.py:78
+msgid "Hostels"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/hostels/browser.py:95
+msgid "Block ${a}, Room ${b}, Bed ${c}"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/containerpage.pt:9
+#: waeup/sirp/hostels/browser_templates/containermanagepage.pt:9
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:35
+msgid "Id"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:37
+msgid "Number"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:56
+msgid "[allocate student]"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:6
+msgid "Hostel Data"
+msgstr ""
+
+#: waeup/sirp/hostels/browser_templates/hostelmanagepage.pt:9
+msgid "Beds"
+msgstr ""
+
+#: waeup/sirp/hostels/hostel.py:180
+msgid "unreserved"
+msgstr ""
+
+#: waeup/sirp/hostels/hostel.py:187
+msgid "reserved"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:105
+msgid "Beds for Returning Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:112
+msgid "Beds for Final Year Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:119
+msgid "Beds without category"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:126
+msgid "Special Handling"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:137
+msgid "Female and male blocks overlap."
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:146
+msgid "Bed categories overlap."
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:169
+msgid "Bed Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:175 waeup/sirp/students/interfaces.py:427
+msgid "Bed Type"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:181
+msgid "Bed Number"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:186 waeup/sirp/hostels/interfaces.py:199
+msgid "Owner (Student)"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:44
+msgid "Hostel Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:49
+msgid "Sort Id"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:55
+msgid "Hostel Name"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:61
+msgid "Floors per Block"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:67
+msgid "Rooms per Floor"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:73
+msgid "Reserved Beds"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:84
+msgid "Blocks for Female Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:91
+msgid "Blocks for Male Students"
+msgstr ""
+
+#: waeup/sirp/hostels/interfaces.py:98
+msgid "Beds for Fresh Students"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:57
+msgid "Bed A"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:58
+msgid "Bed B"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:59
+msgid "Bed C"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:60
+msgid "Bed D"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:61
+msgid "Bed E"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:62
+msgid "Bed F"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:63
+msgid "Bed G"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:64
+msgid "Bed H"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:65
+msgid "Bed I"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:69
+msgid "Block A"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:70
+msgid "Block B"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:71
+msgid "Block C"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:72
+msgid "Block D"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:73
+msgid "Block E"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:74
+msgid "Block F"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:75
+msgid "Block G"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:76
+msgid "Block H"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:77
+msgid "Block I"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:78
+msgid "Block K"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:79
+msgid "Block L"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:80
+msgid "Block M"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:81
+msgid "Block N"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:82
+msgid "Block O"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:83
+msgid "Block P"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:87
+msgid "Regular Hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:88
+msgid "Blocked Hostel"
+msgstr ""
+
+#: waeup/sirp/hostels/vocabularies.py:89
+msgid "Postgraduate Hostel"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:72
+msgid "clearance started"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:73
+msgid "clearance requested"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:74
+msgid "cleared"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:75
+msgid "school fee paid"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:76
+msgid "returning"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:77
+msgid "courses registered"
+msgstr ""
+
+#: waeup/sirp/interfaces.py:78
+msgid "courses validated"
+msgstr ""
+
+#: waeup/sirp/objecthistory.py:73
+msgid "by"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:106
+msgid "Surcharge 1"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:113
+msgid "Surcharge 2"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:120
+msgid "Surcharge 3"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:133
+msgid "Activation Code"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:140
+msgid "Response Amount Approved"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:147
+msgid "Response Description"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:154
+msgid "Response Payment Reference"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:161
+msgid "Response Code"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:168
+msgid "Response Card Number"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:24
+msgid "Not yet paid"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:25
+msgid "Paid"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:26
+msgid "Failed"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:30
+msgid "School Fee"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:31
+msgid "Clearance"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:32
+msgid "Bed Allocation"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:33
+msgid "Hostel Maintenance"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:34
+msgid "Transfer"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:35
+msgid "Gown"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:36
+msgid "Acceptance Fee"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:51
+msgid "Payment Category"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:58
+msgid "Payment Item"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:64
+msgid "Payment State"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:71
+msgid "Ticket Creation Date"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:76
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:10
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:84
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:51
+msgid "Payment Date"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:82
+msgid "Amount Authorized"
+msgstr ""
+
+#: waeup/sirp/payments/interfaces.py:94
+msgid "Payment Access Code"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1003 waeup/sirp/students/browser.py:1225
+msgid "Your current session does not match accommodation session."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1072
+msgid "This ticket has already been paid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1085 waeup/sirp/students/browser.py:1093
+#: waeup/sirp/students/browser.py:1101
+msgid "Valid callback received. ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1155
+msgid "${a}: Accommodation"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1167
+msgid "You are not allowed to remove bed tickets."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:117 waeup/sirp/students/viewlets.py:114
+#: waeup/sirp/students/viewlets.py:534
+msgid "Study Course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1174
+msgid "No bed ticket selected."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1205
+msgid "Add bed ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1207
+msgid "Create bed ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1215
+msgid "Your data are incomplete."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1219
+msgid "You are in the wrong registration state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:123 waeup/sirp/students/viewlets.py:120
+#: waeup/sirp/students/viewlets.py:535
+msgid "Payments"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1230
+msgid "You already booked a bed space in current accommodation session."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1241 waeup/sirp/students/browser.py:1537
+#: waeup/sirp/students/browser.py:1650
+msgid "Activation code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1261 waeup/sirp/students/browser.py:1389
+msgid "There is no free bed in your category ${a}."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1266 waeup/sirp/students/browser.py:1542
+#: waeup/sirp/students/browser.py:1655
+msgid "Activation code has already been used."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1269 waeup/sirp/students/browser.py:1545
+#: waeup/sirp/students/browser.py:1658 waeup/sirp/students/browser.py:1917
+msgid "invalidated"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1274 waeup/sirp/students/browser.py:1549
+#: waeup/sirp/students/browser.py:1663
+msgid "You are not the owner of this access code."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1285 waeup/sirp/students/browser.py:1403
+msgid "${a}, Block ${b}, Room ${c}, Bed ${d} (${e})"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1294
+msgid "Bed ticket created and bed booked: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1312
+msgid "Bed Ticket for Session ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1328
+msgid "Bed Allocation Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1334
+msgid "Bed Allocation: "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1361
+msgid "Students in reserved beds can't be relocated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1367
+msgid "Student can't be relocated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:138 waeup/sirp/students/viewlets.py:126
+msgid "Accommodation"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1410
+msgid "Student relocated: ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1429
+msgid "${a}: History"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1441 waeup/sirp/students/viewlets.py:356
+msgid "Edit base data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1456 waeup/sirp/students/viewlets.py:365
+msgid "Change password"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:147
+msgid "Bed Ticket ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1470
+msgid "Password changed."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1482
+msgid "Upload portrait"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1497 waeup/sirp/students/viewlets.py:389
+#: waeup/sirp/students/workflow.py:44
+msgid "Start clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1501
+msgid "Start clearance now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1518 waeup/sirp/students/browser.py:1639
+msgid "Wrong state"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1522
+msgid "No portrait uploaded."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1526
+msgid "Not all required fields filled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1553
+msgid "Clearance process has been started."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1565
+msgid "Edit clearance data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1577 waeup/sirp/students/browser.py:1591
+msgid "Clearance form has been saved."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1585
+msgid "Save and request clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1600 waeup/sirp/students/workflow.py:58
+msgid "Request clearance"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1601
+msgid "Enter the CLR access code used for starting clearance."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1604
+msgid "Request clearance now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1613
+msgid "This isn't your CLR access code."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1622
+msgid "Clearance has been requested."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1631 waeup/sirp/students/viewlets.py:418
+msgid "Start course registration"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1635
+msgid "Start course registration now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1671
+msgid "Course registration has been started."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1690
+msgid "Add current level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1699
+msgid "Create course list now"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:170
+msgid "Student Section"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1738
+msgid "Add and remove course tickets of study level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1775
+msgid "Register course list"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1779
+msgid "Course list has been registered."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1825
+msgid "Change my password"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1835
+msgid "Get new login credentials"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1849
+msgid "No student record found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1857
+msgid "You have successfully changed your password for the"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:186 waeup/sirp/students/browser.py:222
+msgid "Empty search string"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1862
+msgid "An email with your user name and password has been sent to ${a}."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1873
+msgid "Set password for first-time login"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1876
+msgid "Set"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1891
+msgid "More than one student found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1899
+msgid "Access code is invalid."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1902
+msgid "Password has already been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1908
+msgid "Password has already been set. You are using the wrong Access Code."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:1914
+msgid "Access code has already been used."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:1923
+msgid "Password has been set. Your Student Id is ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:204 waeup/sirp/students/viewlets.py:139
+msgid "Manage student section"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:228 waeup/sirp/students/browser.py:1888
+msgid "No student found."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:255 waeup/sirp/students/viewlets.py:146
+msgid "Add student"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:263
+msgid "Student record created."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:279
+msgid "${a}: Base Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:322 waeup/sirp/students/browser.py:1865
+msgid "An smtp server error occurred."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:333
+msgid "Manage base data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:402
+msgid "${a}: Clearance Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:418 waeup/sirp/students/viewlets.py:104
+#: waeup/sirp/students/viewlets.py:532
+#: waeup/sirp/students/browser_templates/clearanceeditpage.pt:6
+msgid "Clearance Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:424
+msgid "Clearance Slip of "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:444
+msgid "Manage clearance data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:473
+msgid "Student has been cleared."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:475 waeup/sirp/students/browser.py:499
+msgid "Student is in wrong state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:492
+msgid "Clearance has been annulled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:496
+msgid "Clearance request has been rejected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:522
+msgid "${a}: Personal Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:532
+msgid "Manage personal data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:552
+msgid "${a}: Study Course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:582
+msgid "Manage study course"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:585 waeup/sirp/students/browser.py:626
+msgid "Remove selected levels"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:586 waeup/sirp/students/browser.py:612
+msgid "Add study level"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:620
+msgid "Study level has been added."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:622 waeup/sirp/students/browser.py:1708
+msgid "This level exists."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:632
+msgid "No study level selected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:668
+msgid "${a}: Study Level ${b}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:691
+msgid "Level Data"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:697
+msgid "Course List"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:706
+msgid "Course Registration Slip  "
+msgstr ""
+
+#: waeup/sirp/students/browser.py:712
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:35
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:12
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:33
+msgid "Sem."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:715
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:38
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:36
+msgid "Dept."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:716
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:16
+msgid "Faculty"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:717
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:40
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:38
+msgid "Cred."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:718
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:41
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:39
+msgid "Mand."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:719 waeup/sirp/students/interfaces.py:370
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:42
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:18
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:40
+msgid "Score"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:747 waeup/sirp/students/browser.py:776
+#: waeup/sirp/students/browser.py:861 waeup/sirp/students/browser.py:866
+#: waeup/sirp/students/browser.py:1748 waeup/sirp/students/browser.py:1798
+msgid "Add course ticket"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:768
+msgid "Manage study level ${a}"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:786 waeup/sirp/students/browser.py:1758
+msgid "No ticket selected."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:810 waeup/sirp/students/browser.py:832
+msgid "This level does not correspond current level."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:814
+msgid "Course list has been validated."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:816 waeup/sirp/students/browser.py:844
+msgid "Student is in the wrong state."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:837
+msgid "Course list request has been annulled."
+msgstr ""
+
+#: waeup/sirp/students/browser.py:841
+msgid "Course list request has been rejected:"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:881 waeup/sirp/students/browser.py:1811
+msgid "The ticket exists."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:883 waeup/sirp/students/browser.py:1813
+msgid "Successfully added ${a}."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:904
+msgid "${a}: Course Ticket ${b}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:920
+msgid "Manage course ticket ${a}"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/browser.py:947
+msgid "${a}: Payments"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:992
+msgid "Add online payment"
+msgstr ""
+
+#: waeup/sirp/students/browser.py:995
+msgid "Create ticket"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:11
+msgid "Bed Category"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:29
+msgid "Registration Transition:"
+msgstr ""
+
+#. Default: "Please solve the captcha<br />to prevent misuse of this service:"
+#: waeup/sirp/students/browser_templates/changepw.pt:8
+msgid "solve_captcha"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/clearanceeditpage.pt:8
+msgid "Scans"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:28
+msgid "Select all"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:10
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:10
+msgid "with registration number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:11
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:11
+msgid "with matriculation number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:12
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:12
+msgid "in session"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:13
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:13
+msgid "in department"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:14
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:14
+msgid "in study mode"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:30
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:36
+msgid "Reg. Number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:31
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:37
+msgid "Matric. Number"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:32
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:38
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:14
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:25
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:87
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:54
+msgid "State"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:33
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:39
+#: waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt:44
+msgid "Full Name"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:5
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:5
+msgid "for students"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:8
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:8
+msgid "with id"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/containerpage.pt:9
+#: waeup/sirp/students/browser_templates/containermanagepage.pt:9
+msgid "with name"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:31
+msgid "Semester:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:39
+msgid "Credits:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/courseticketmanagepage.pt:47
+msgid "Passmark:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/enterpin.pt:5
+msgid "Activation Code:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/imageupload.pt:20
+#: waeup/sirp/students/browser_templates/fileupload.pt:27
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:25
+msgid "Max. file size:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:11
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:85
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:52
+msgid "Category"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:12
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:86
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:53
+msgid "Item"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:8
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:82
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:49
+msgid "Payment Id"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:9
+#: waeup/sirp/applicants/browser_templates/applicanteditpage.pt:83
+#: waeup/sirp/applicants/browser_templates/applicantdisplaypage.pt:50
+msgid "Creation Date"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/setpassword.pt:14
+msgid "Access Code:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/setpassword.pt:32
+msgid "Instructions:"
+msgstr ""
+
+#: waeup/sirp/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/sirp/students/browser_templates/setpassword.pt:5
+msgid "Registration Number:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:6
+msgid "Study Course Data"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:9
+msgid "Study Levels"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:18
+msgid "Study Mode:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:52
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:30
+msgid "Study Levels (Course Lists)"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:57
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:35
+msgid "Level Code"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:58
+#: waeup/sirp/students/browser_templates/studycoursemanagepage.pt:36
+msgid "Level Title"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studycoursepage.pt:71
+msgid "There are no levels registered yet."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveladdpage.pt:4
+msgid "Text to explain course registration procedure."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:15
+msgid "Department"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:6
+msgid "Course Tickets (Total Credits: ${DYNAMIC_CONTENT})"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:39
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:37
+msgid "Fact."
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:43
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:19
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:41
+msgid "CO"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:44
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:42
+msgid "Auto"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:6
+msgid "Study Level Data"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:9
+#: waeup/sirp/students/browser_templates/studylevelmanagepage.pt:30
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:28
+msgid "Course Tickets"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:18
+msgid "Total Credits:"
+msgstr ""
+
+#: waeup/sirp/students/browser_templates/studylevelpage.pt:64
+msgid "The're no tickets added yet."
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:176 waeup/sirp/students/interfaces.py:255
+#: waeup/sirp/students/interfaces.py:483
+msgid "Registration Number"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:183 waeup/sirp/students/interfaces.py:265
+msgid "Matriculation Number"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:191
+msgid "PWD Activation Code"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:198
+msgid "Email"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:218
+msgid "Clearance form locked"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:223
+msgid "CLR Activation Code"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:231
+msgid "Nationality"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:241
+msgid "Permanent Address"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:275
+#: waeup/sirp/browser/templates/showstudentspage.pt:7
+msgid "Certificate"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:282
+msgid "Entry Mode"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:290
+msgid "Entry Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:299
+#: waeup/sirp/browser/templates/showstudentspage.pt:8
+msgid "Current Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:307
+#: waeup/sirp/browser/templates/showstudentspage.pt:9
+msgid "Current Level"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:315
+msgid "Current Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:322
+msgid "Previous Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:337 waeup/sirp/students/interfaces.py:434
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:8
+#: waeup/sirp/students/browser_templates/paymentsmanagepage.pt:13
+msgid "Session"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:344
+msgid "Verdict"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:363
+#: waeup/sirp/browser/templates/certificatepage.pt:30
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:33
+msgid "Mandatory"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:377
+msgid "Automatical Creation"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:384
+msgid "Carry-over Course"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:403
+#: waeup/sirp/university/interfaces.py:299
+#: waeup/sirp/browser/templates/certificatepage.pt:29
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:32
+msgid "Course"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:420
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:10
+msgid "Bed Coordinates"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:442
+#: waeup/sirp/students/browser_templates/accommodationmanagepage.pt:9
+msgid "Booking Date"
+msgstr ""
+
+#: waeup/sirp/students/interfaces.py:448
+msgid "Booking Activation Code"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/utils.py:163
+msgid "<font size=10>${a} Page ${b} of ${c}</font>"
+msgstr ""
+
+#: waeup/sirp/students/utils.py:276 waeup/sirp/students/viewlets.py:71
+#: waeup/sirp/students/viewlets.py:99
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:6
+msgid "Base Data"
+msgstr ""
+
+#: waeup/sirp/students/utils.py:307
+msgid "Scanned Documents"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:109 waeup/sirp/students/viewlets.py:533
+msgid "Personal Data"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:131 waeup/sirp/students/viewlets.py:537
+#: waeup/sirp/accesscodes/browser_templates/searchpage.pt:27
+msgid "History"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:179 waeup/sirp/students/workflow.py:72
+msgid "Clear student"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:194
+msgid "Reject clearance"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:210
+msgid "Download clearance slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:235
+msgid "Download course registration slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:251 waeup/sirp/students/workflow.py:128
+msgid "Validate courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:267
+msgid "Reject courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:330
+msgid "Book accommodation"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:339
+msgid "Download bed allocation slip"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:348
+msgid "Relocate student"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:374
+msgid "Change portrait"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:432
+msgid "Add course list"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:450
+msgid "Add and remove courses"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:536
+msgid "Accommodation Data"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:548
+msgid "${a} deleted."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:561
+msgid "File name has no extension."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:564
+msgid "File name contains more than one dot."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:571
+msgid "${a} file extension expected."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:577
+msgid "Only the following extension are allowed: ${a}"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:583
+msgid "Uploaded file is too big."
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/viewlets.py:590
+msgid "File ${a} uploaded."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:608
+#: waeup/sirp/browser/templates/datacenterlogspage.pt:10
+#: waeup/sirp/browser/templates/datacenterpage.pt:14
+#: waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt:11
+msgid "File"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:609
+msgid "Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:632
+msgid "Upload new file"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:633
+msgid "Delete attachment"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:663
+msgid "No local file selected."
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:677
+#: waeup/sirp/students/browser_templates/basemanagepage.pt:8
+msgid "Passport Picture"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:688
+msgid "Passport Picture (jpg only)"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:703 waeup/sirp/students/viewlets.py:714
+msgid "Birth Certificate"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:704 waeup/sirp/students/viewlets.py:715
+msgid "Birth Certificate Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:724 waeup/sirp/students/viewlets.py:735
+msgid "Acceptance Letter"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:725 waeup/sirp/students/viewlets.py:736
+msgid "Acceptance Letter Scan"
+msgstr ""
+
+#: waeup/sirp/students/viewlets.py:85
+msgid "Application Slip"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:135
+msgid "Male"
+msgstr ""
+
+#: waeup/sirp/students/vocabularies.py:137
+msgid "Female"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:77
+msgid "Error: level id ${value} out of range"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:88
+msgid "${title} 2nd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:91
+msgid "${title} 3rd spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:94
+msgid "${title} 1st spillover"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:96
+msgid "${title} on 1st probation"
+msgstr ""
+
+#. Default: ""
+#: waeup/sirp/students/vocabularies.py:98
+msgid "${title} on 2nd probation"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:107
+msgid "Reset to returning"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:108
+msgid "Student record reset to 'returning'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:114
+msgid "Register courses"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:115
+msgid "Courses registered"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:121 waeup/sirp/students/workflow.py:135
+msgid "Reset to paid"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:122 waeup/sirp/students/workflow.py:136
+msgid "Student record reset to 'paid'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:129
+msgid "Courses validated"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:142
+msgid "Return"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:143
+msgid "Returned"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:149
+msgid "Reset to validated"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:150
+msgid "Student record reset to 'validated'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:22
+msgid "Create student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:30
+msgid "Admit student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:31
+msgid "Student admitted"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:37
+msgid "Reset student"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:38
+msgid "Student record reset"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:45
+msgid "Clearance started"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:51
+msgid "Reset to admitted"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:52
+msgid "Student record reset to 'admitted'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:59
+msgid "Clearance requested"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:65 waeup/sirp/students/workflow.py:79
+msgid "Reset to clearance"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:66 waeup/sirp/students/workflow.py:80
+msgid "Student record reset to 'clearance'"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:73
+msgid "Cleared"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:86 waeup/sirp/students/workflow.py:100
+msgid "Pay school fee"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:87 waeup/sirp/students/workflow.py:101
+msgid "School fee paid"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:93
+msgid "Reset to cleared"
+msgstr ""
+
+#: waeup/sirp/students/workflow.py:94
+msgid "Student record reset to 'cleared'"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:146
+msgid "Title of course"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:152
+#: waeup/sirp/students/browser_templates/studyleveleditpage.pt:17
+msgid "Credits"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:158
+msgid "Passmark"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:164
+msgid "Semester/Term"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:180
+msgid "Enter unique course code which will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:205
+msgid "Study Mode"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:212
+msgid "Start Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:219
+msgid "End Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:226
+msgid "Application Category"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:243
+msgid "Enter unique certificate code which will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:264
+msgid "Course Referrer"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:270
+#: waeup/sirp/browser/templates/certificatepage.pt:28
+#: waeup/sirp/browser/templates/certificatemanagepage.pt:30
+msgid "Level"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:277
+msgid "Is mandatory course (not elective)"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:45
+msgid "Name of faculty"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:46
+#: waeup/sirp/university/interfaces.py:94
+#: waeup/sirp/university/interfaces.py:147
+#: waeup/sirp/university/interfaces.py:200
+msgid "Unnamed"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:51
+#: waeup/sirp/university/interfaces.py:99
+msgid "Name prefix"
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:67
+#: waeup/sirp/university/interfaces.py:118
+msgid "This code will become part of the URL."
+msgstr ""
+
+#: waeup/sirp/university/interfaces.py:93
+msgid "Name of department"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:28
+msgid "Pre-Studies"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:29
+msgid "100 (Year 1)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:30
+msgid "200 (Year 2)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:31
+msgid "300 (Year 3)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:32
+msgid "400 (Year 4)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:33
+msgid "500 (Year 5)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:34
+msgid "600 (Year 6)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:35
+msgid "700 (Year 7)"
+msgstr ""
+
+#: waeup/sirp/university/vocabularies.py:36
+msgid "800 (Year 8)"
+msgstr ""
+
+#: waeup/sirp/utils/utils.py:119
+msgid ""
+"Fullname: ${a}\n"
+"User Id: ${b}\n"
+"User Type: ${c}\n"
+"Portal: ${d}\n"
+"\n"
+"${e}\n"
+msgstr ""
+
+#: waeup/sirp/utils/utils.py:164
+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"
+"Login page: ${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 ""
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/mail.zcml
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/mail.zcml	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/mail.zcml	(revision 7760)
@@ -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 SIRP 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/meta.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/meta.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/meta.py	(revision 7760)
@@ -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 SIRP 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.sirp.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 = SIRPIndexesUpgradeSubscriber(
+            catalog_name, indexes, context, module_info)
+        subscribed = (site, IObjectUpgradeEvent)
+        config.action(
+            discriminator=None,
+            callable=component.provideHandler,
+            args=(subscriber, subscribed),
+            )
+        return True
+
+
+class SIRPIndexesUpgradeSubscriber(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('Index created.')
+            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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/meta.zcml
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/meta.zcml	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/meta.zcml	(revision 7760)
@@ -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/sirp"
+      name="datacenter"
+      schema=".zcml.IDataCenterConfig"
+      handler=".zcml.data_center_conf"
+      />
+
+
+</configure>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/objecthistory.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/objecthistory.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/objecthistory.py	(revision 7760)
@@ -0,0 +1,78 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+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.sirp.interfaces import IObjectHistory, ISIRPObject, ISIRPUtils
+from waeup.sirp.utils.helpers import get_current_principal
+
+from waeup.sirp.interfaces import MessageFactory as _
+
+class ObjectHistory(grok.Adapter):
+    """A history for objects.
+
+    For any object implementing `ISIRPObject` which is annotatable,
+    we provide histories. A history for such an object can be obtained
+    by adapting it to `IObjectHistory`.
+    """
+    grok.context(ISIRPObject)
+    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()
+        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+        user = get_current_principal()
+        if user is None:
+            user = 'system'
+        elif user.id == 'zope.anybody':
+            user = 'Anonymous'
+        else:
+            user = user.title
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        by = 'by'
+        if portal_language != 'en':
+            by = translate(_('by'),'waeup.sirp',target_language=portal_language)
+            msg = translate(msg,'waeup.sirp',target_language=portal_language)
+        msg = u'%s - %s %s %s' % (timestamp, msg, by, user)
+        msgs.append(msg)
+        self._annotations[self.history_key] = msgs
+        return
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/__init__.py	(revision 7760)
@@ -0,0 +1,11 @@
+"""This package contains everything regarding payments.
+"""
+# Make this a package.
+from waeup.sirp.payments.container import PaymentsContainer
+from waeup.sirp.payments.payment import SCPayment, OnlinePayment
+
+__all__ = [
+    'PaymentsContainer',
+    'SCPayment',
+    'OnlinePayment',
+    ]
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/container.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/container.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/container.py	(revision 7760)
@@ -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.sirp.payments.interfaces import IPaymentsContainer
+from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/interfaces.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/interfaces.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/interfaces.py	(revision 7760)
@@ -0,0 +1,172 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.interfaces import ISIRPObject, SimpleSIRPVocabulary
+from waeup.sirp.interfaces import MessageFactory as _
+
+payment_states = SimpleSIRPVocabulary(
+    (_('Not yet paid'),'unpaid'),
+    (_('Paid'),'paid'),
+    (_('Failed'),'failed'),
+    )
+
+payment_categories = SimpleSIRPVocabulary(
+    (_('School Fee'),'schoolfee'),
+    (_('Clearance'),'clearance'),
+    (_('Bed Allocation'),'bed_allocation'),
+    (_('Hostel Maintenance'),'hostel_maintenance'),
+    (_('Transfer'),'transfer'),
+    (_('Gown'),'gown'),
+    (_('Acceptance Fee'), 'acceptance'),
+    )
+
+class IPaymentsContainer(ISIRPObject):
+    """A container for all kind of payment objects.
+
+    """
+
+class IPayment(ISIRPObject):
+    """A base representation of payments.
+
+    """
+    p_id = Attribute('Payment identifier.')
+
+    p_category = schema.Choice(
+        title = _(u'Payment Category'),
+        default = u'schoolfee',
+        vocabulary = payment_categories,
+        required = True,
+        )
+
+    p_item = schema.TextLine(
+        title = _(u'Payment Item'),
+        default = None,
+        required = False,
+        )
+
+    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,
+        )
+
+    payment_date = schema.Datetime(
+        title = _(u'Payment Date'),
+        required = False,
+        readonly = False,
+        )
+
+    amount_auth = schema.Int(
+        title = _(u'Amount Authorized'),
+        default = 0,
+        required = True,
+        readonly = True,
+        )
+
+class ISCPayment(IPayment):
+    """A scratch card payment.
+
+    """
+
+    p_code = schema.TextLine(
+        title = _(u'Payment Access Code'),
+        #default = u'Certificate XYZ',
+        required = False,
+        readonly = True,
+        )
+
+class IOnlinePayment(IPayment):
+    """A payment via payment gateways.
+
+    """
+
+    surcharge_1 = schema.Int(
+        title = _(u'Surcharge 1'),
+        default = 0,
+        required = False,
+        readonly = True,
+        )
+
+    surcharge_2 = schema.Int(
+        title = _(u'Surcharge 2'),
+        default = 0,
+        required = False,
+        readonly = True,
+        )
+
+    surcharge_3 = schema.Int(
+        title = _(u'Surcharge 3'),
+        default = 0,
+        required = False,
+        readonly = True,
+        )
+
+    #order_id = schema.TextLine(
+    #    title = u'Order Id',
+    #    default = None,
+    #    required = True,
+    #    )
+
+    ac = schema.TextLine(
+        title = _(u'Activation Code'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    r_amount_approved = schema.Int(
+        title = _(u'Response Amount Approved'),
+        default = 0,
+        required = False,
+        readonly = False,
+        )
+
+    r_desc = schema.TextLine(
+        title = _(u'Response Description'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    r_pay_reference = schema.TextLine(
+        title = _(u'Response Payment Reference'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    r_code = schema.TextLine(
+        title = _(u'Response Code'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    r_card_num = schema.TextLine(
+        title = _(u'Response Card Number'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/payment.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/payment.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/payment.py	(revision 7760)
@@ -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
+##
+"""
+These are the payment tickets.
+"""
+import grok
+from datetime import datetime
+from grok import index
+from waeup.sirp.payments.interfaces import (
+    IPayment, ISCPayment, IOnlinePayment,
+    payment_states, payment_categories)
+from waeup.sirp.utils.helpers import attrs_to_fields
+
+class Payment(grok.Container):
+    """This is a payment.
+    """
+    grok.implements(IPayment)
+    grok.provides(IPayment)
+    grok.baseclass()
+
+    def __init__(self):
+        super(Payment, self).__init__()
+        self.creation_date = datetime.now()
+        self.p_id = None
+        return
+
+    @property
+    def state(self):
+        return payment_states.getTermByToken(self.p_state).title
+
+    @property
+    def category(self):
+        return payment_categories.getTermByToken(self.p_category).title
+
+class SCPayment(Payment):
+    """This is a scratch card payment.
+    """
+    grok.implements(ISCPayment)
+    grok.provides(ISCPayment)
+
+    def __init__(self):
+        super(SCPayment, self).__init__()
+        p_id = None
+        return
+
+SCPayment = attrs_to_fields(SCPayment)
+
+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
+
+OnlinePayment = attrs_to_fields(OnlinePayment)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/tests.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/tests.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/payments/tests.py	(revision 7760)
@@ -0,0 +1,67 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.payments.interfaces import (
+    IPaymentsContainer, ISCPayment, IOnlinePayment)
+from waeup.sirp.payments.container import PaymentsContainer
+from waeup.sirp.payments.payment import (
+    SCPayment, OnlinePayment)
+from waeup.sirp.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(
+                ISCPayment, SCPayment)
+            )
+        self.assertTrue(
+            verifyClass(
+                IOnlinePayment, OnlinePayment)
+            )
+        self.assertTrue(
+            verifyObject(
+                ISCPayment, SCPayment())
+            )
+        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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/permissions.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/permissions.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/permissions.py	(revision 7760)
@@ -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
+##
+import grok
+from zope.component import getUtilitiesFor
+from zope.interface import Interface
+from zope.securitypolicy.interfaces import IRole, IPrincipalRoleMap
+from waeup.sirp.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 ManageUniversity(grok.Permission):
+    grok.name('waeup.manageUniversity')
+
+class ManageUsers(grok.Permission):
+    grok.name('waeup.manageUsers')
+
+class ShowStudents(grok.Permission):
+    grok.name('waeup.showStudents')
+
+class EditUser(grok.Permission):
+    grok.name('waeup.editUser')
+
+class ManageDataCenter(grok.Permission):
+    grok.name('waeup.manageDataCenter')
+
+class ManagePortalConfiguration(grok.Permission):
+    grok.name('waeup.managePortalConfiguration')
+
+class ManageACBatches(grok.Permission):
+    grok.name('waeup.manageACBatches')
+
+# Local Roles
+class DepartmentManager(grok.Role):
+    grok.name('waeup.local.DepartmentManager')
+    grok.title(u'Department Manager')
+    grok.permissions('waeup.manageUniversity','waeup.showStudents')
+
+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')
+
+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')
+
+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')
+
+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')
+
+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')
+
+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')
+
+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')
+
+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 ACManager(grok.Role):
+    grok.name('waeup.ACManager')
+    grok.title(u'Access Code Manager')
+    grok.permissions('waeup.manageACBatches')
+
+class PortalManager(grok.Role):
+    grok.name('waeup.PortalManager')
+    grok.title(u'Portal Manager')
+    grok.permissions('waeup.manageUniversity', 'waeup.manageUsers',
+                     'waeup.viewAcademics', 'waeup.manageACBatches',
+                     'waeup.manageDataCenter','waeup.managePortalSettings',
+                     'waeup.managePortalConfiguration', 'waeup.viewApplication',
+                     'waeup.manageApplication', 'waeup.handleApplication',
+                     'waeup.viewApplicantsTab', 'waeup.payApplicant',
+                     'waeup.viewStudent', 'waeup.manageStudent',
+                     'waeup.clearStudent', 'waeup.payStudent',
+                     'waeup.uploadStudentFile', 'waeup.showStudents',
+                     'waeup.viewStudentsContainer','waeup.viewStudentsTab',
+                     'waeup.viewHostels', 'waeup.manageHostels',
+                     )
+
+def get_all_roles():
+    """Return a list of tuples ``<ROLE-NAME>, <ROLE>``.
+    """
+    return getUtilitiesFor(IRole)
+
+def get_waeup_roles(also_local=False):
+    """Get all SIRP roles.
+
+    SIRP 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 SIRP 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-SIRP 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 SIRP roles.
+
+    See :func:`get_waeup_roles` for what a 'SIRPRole' is.
+
+    This function returns a sorted list of SIRP role names.
+    """
+    return sorted([x.id for x in get_waeup_roles()])
+
+class LocalRolesAssignable(grok.Adapter):
+    """Default implementation for `ILocalRolesAssignable`.
+
+    This adapter returns a list for dictionaries for objects for which
+    we want to know the roles assignable to them locally.
+
+    The returned dicts contain a ``name`` and a ``title`` entry which
+    give a role (``name``) and a description, for which kind of users
+    the permission is meant to be used (``title``).
+
+    Having this adapter registered we make sure, that for each normal
+    object we get a valid `ILocalRolesAssignable` adapter.
+
+    Objects that want to offer certain local roles, can do so by
+    setting a (preferably class-) attribute to a list of role ids.
+
+    You can also define different adapters for different contexts to
+    have different role lookup mechanisms become available. But in
+    normal cases it should be sufficient to use this basic adapter.
+    """
+    grok.context(Interface)
+    grok.provides(ILocalRolesAssignable)
+
+    _roles = []
+
+    def __init__(self, context):
+        self.context = context
+        role_ids = getattr(context, 'local_roles', self._roles)
+        self._roles = [(name, role) for name, role in get_all_roles()
+                       if name in role_ids]
+        return
+
+    def __call__(self):
+        """Get a list of dictionaries containing ``names`` (the roles to
+        assign) and ``titles`` (some description of the type of user
+        to assign each role to).
+        """
+        list_of_dict = [dict(
+                name=name,
+                title=role.title,
+                description=role.description)
+                for name, role in self._roles]
+        return sorted(list_of_dict, key=lambda x: x['name'])
+
+def get_users_with_local_roles(context):
+    """Get a list of dicts representing the local roles set for `context`.
+
+    Each dict returns `user_name`, `user_title`, `local_role`,
+    `local_role_title`, and `setting` for each entry in the local
+    roles map of the `context` object.
+    """
+    try:
+        role_map = IPrincipalRoleMap(context)
+    except TypeError:
+        # no map no roles.
+        raise StopIteration
+    for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
+        user = grok.getSite()['users'].get(user_name,None)
+        user_title = getattr(user, 'title', user_name)
+        local_role_title = dict(get_all_roles())[local_role].title
+        yield dict(user_name = user_name,
+                   user_title = user_title,
+                   local_role = local_role,
+                   local_role_title = local_role_title,
+                   setting = setting)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/permissions.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/permissions.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/permissions.txt	(revision 7760)
@@ -0,0 +1,84 @@
+SIRP permissions and roles
+**************************
+
+Permissions and roles used in a SIRP portal.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.SIRPUnitTestLayer
+
+Convenience functions
+=====================
+
+:mod:`waeup.sirp` offers some convenience functions to handle security
+roles.
+
+:func:`get_all_roles`
+---------------------
+
+Gives us all roles defined in SIRP. 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.sirp.permissions import get_all_roles
+    >>> get_all_roles()
+    <generator object...at 0x...>
+
+    >>> sorted(list(get_all_roles()))
+    [(u'waeup.ACManager', <waeup.sirp.permissions.ACManager object at 0x...]
+
+:func:`get_waeup_roles`
+-----------------------
+
+Gives us all roles, except the SIRP specific roles. We can get a list
+with or without local roles:
+
+    >>> from waeup.sirp.permissions import get_waeup_roles
+    >>> len(list(get_waeup_roles()))
+    11
+
+    >>> len(list(get_waeup_roles(also_local=True)))
+    22
+
+
+:func:`get_waeup_role_names`
+----------------------------
+
+We can get all role names defined in SIRP (except 'local'
+roles that are meant not to be assigned globally):
+
+    >>> from waeup.sirp.permissions import get_waeup_role_names
+    >>> list(get_waeup_role_names())
+    [u'waeup.ACManager',
+     u'waeup.AcademicsOfficer',
+     u'waeup.AccommodationOfficer',
+     u'waeup.Applicant',
+     u'waeup.ApplicationsOfficer',
+     u'waeup.PortalManager',
+     u'waeup.Student',
+     u'waeup.StudentsClearanceOfficer',
+     u'waeup.StudentsCourseAdviser',
+     u'waeup.StudentsManager',
+     u'waeup.StudentsOfficer']
+
+: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.sirp.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
+    []
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/schema/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/schema/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/schema/__init__.py	(revision 7760)
@@ -0,0 +1,3 @@
+from waeup.sirp.schema.field import *
+
+__all__ = tuple(name for name in globals() if not name.startswith('_'))
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/schema/field.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/schema/field.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/schema/field.py	(revision 7760)
@@ -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
+##
+"""Special fields.
+"""
+from zope.interface import implements
+from zope.schema import TextLine
+from zope.schema.interfaces import (
+    ITextLine, IBaseVocabulary, ISource, IContextSourceBinder, InvalidValue,)
+from zope.schema.vocabulary import (
+    SimpleVocabulary, getVocabularyRegistry, VocabularyRegistryError)
+
+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
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/schema/tests/test_textlinechoice.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/schema/tests/test_textlinechoice.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/schema/tests/test_textlinechoice.py	(revision 7760)
@@ -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.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/smtp.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/smtp.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/smtp.py	(revision 7760)
@@ -0,0 +1,154 @@
+## $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 SIRP.
+
+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.sirp.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):
+    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_addr,
+              subject, body, config=None):
+    """Send mail.
+    """
+    # format message
+    body = encode_body(body)
+    body["From"] = encode_address(from_addr, from_name)
+    body["To"] = encode_address(rcpt_addr, rcpt_name)
+    body["Subject"] = encode_header_item(subject)
+
+    mailer = getUtility(IMailService)
+    result = mailer().send(from_addr, [rcpt_addr], body.as_string())
+    return result
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/__init__.py	(revision 7760)
@@ -0,0 +1,22 @@
+"""This package contains everything regarding students.
+"""
+# Make this a package.
+from waeup.sirp.students.student import (
+    Student, StudentFactory
+    )
+from waeup.sirp.students.container import StudentsContainer
+from waeup.sirp.students.studycourse import StudentStudyCourse
+from waeup.sirp.students.payments import StudentPaymentsContainer
+from waeup.sirp.students.accommodation import StudentAccommodation
+from waeup.sirp.students.dynamicroles import StudentPrincipalRoleManager
+
+
+__all__ = [
+    'Student',
+    'StudentFactory',
+    'StudentsContainer',
+    'StudentStudyCourse',
+    'StudentPaymentsContainer',
+    'StudentAccommodation',
+    'StudentPrincipalRoleManager',
+    ]
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/accommodation.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/accommodation.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/accommodation.py	(revision 7760)
@@ -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
+##
+"""
+Student accommodation components.
+"""
+from datetime import datetime
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.sirp.interfaces import academic_sessions_vocab
+from waeup.sirp.students.interfaces import (
+    IStudentAccommodation, IStudentNavigation, IBedTicket)
+from waeup.sirp.utils.helpers import attrs_to_fields
+
+class StudentAccommodation(grok.Container):
+    """This is a container for accommodation objects.
+    """
+    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[bedticket.ticket_id] = bedticket
+        return
+
+    def getStudent(self):
+        return self.__parent__
+
+StudentAccommodation = attrs_to_fields(StudentAccommodation)
+
+class BedTicket(grok.Model):
+    """This is a bed ticket which shows that the student has booked a bed
+    and paid the maintenance fee.
+    """
+    grok.implements(IBedTicket, IStudentNavigation)
+    grok.provides(IBedTicket)
+
+    def __init__(self):
+        super(BedTicket, self).__init__()
+        self.booking_date = datetime.now()
+        self.bed = None
+        return
+
+    def getStudent(self):
+        return self.__parent__.__parent__
+
+    def getSessionString(self):
+        return academic_sessions_vocab.getTerm(
+            self.booking_session).title
+
+BedTicket = attrs_to_fields(BedTicket)
+
+# 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/authentication.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/authentication.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/authentication.py	(revision 7760)
@@ -0,0 +1,289 @@
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public 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
+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.sirp.authentication import (
+    SIRPPrincipalInfo, get_principal_role_manager)
+from waeup.sirp.interfaces import (
+    IAuthPluginUtility, IUserAccount, IPasswordValidator)
+from waeup.sirp.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)
+
+    @property
+    def name(self):
+        return self.context.student_id
+
+    @property
+    def password(self):
+        return getattr(self.context, 'password', None)
+
+    @property
+    def title(self):
+        return self.context.display_fullname
+
+    @property
+    def email(self):
+        return self.context.email
+
+    @property
+    def phone(self):
+        return self.context.phone
+
+    @property
+    def user_type(self):
+        return u'student'
+
+    @property
+    def description(self):
+        return self.title
+
+    def _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 = u'%s' % (
+            passwordmanager.encodePassword(password))
+
+    def checkPassword(self, password):
+        """Check whether the given `password` matches the one stored.
+        """
+        if not isinstance(password, basestring):
+            return False
+        if not getattr(self.context, 'password', None):
+            # unset/empty passwords do never match
+            return False
+        passwordmanager = getUtility(IPasswordManager, 'SSHA')
+        return passwordmanager.checkPassword(
+            self.context.password.encode('utf-8'), # turn unicode into bytes
+            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 SIRPPrincipalInfo(id=account.name,
+                             title=account.title,
+                             description=account.description,
+                             email=account.email,
+                             phone=account.phone,
+                             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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/batching.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/batching.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/batching.py	(revision 7760)
@@ -0,0 +1,640 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 csv
+from zope.interface import Interface
+from zope.schema import getFields
+from zope.component import queryUtility, getUtility
+from zope.event import notify
+from zope.catalog.interfaces import ICatalog
+from hurry.workflow.interfaces import IWorkflowState
+from waeup.sirp.interfaces import (
+    IBatchProcessor, FatalCSVError, IObjectConverter, IUserAccount,
+    IObjectHistory)
+from waeup.sirp.students.interfaces import (
+    IStudent, IStudentStudyCourse,
+    IStudentUpdateByRegNo, IStudentUpdateByMatricNo,
+    IStudentStudyLevel, ICourseTicket,
+    IStudentOnlinePayment)
+from waeup.sirp.students.workflow import  IMPORTABLE_STATES
+from waeup.sirp.utils.batching import BatchProcessor
+
+class StudentProcessor(BatchProcessor):
+    """A batch processor for IStudent objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'studentimporter'
+    grok.name(util_name)
+
+    name = u'Student Importer'
+    iface = IStudent
+
+    location_fields = []
+    factory_name = 'waeup.Student'
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+            ['student_id','reg_number','matric_number',
+            'password', 'reg_state'] + getFields(
+                self.iface).keys())))
+
+    def checkHeaders(self, headerfields, mode='create'):
+        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):
+            return 'student_id'
+        elif row.get('reg_number',None):
+            return 'reg_number'
+        elif row.get('matric_number',None):
+            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)
+        # We don't need to log this because is already done by the workflow
+        # transition even handler.
+        history = IObjectHistory(obj)
+        history.addMessage('Student record imported')
+        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 updateEntry(self, obj, row, site):
+        """Update obj to the values given in row.
+        """
+        # Remove student_id from row if empty
+        if row.has_key('student_id') and row['student_id'] is None:
+            row.pop('student_id')
+        items_changed = ''
+        for key, value in row.items():
+            # Set student password and all fields declared in interface.
+            if key == 'password' and value != '':
+                IUserAccount(obj).setPassword(value)
+            elif key == 'reg_state':
+                IWorkflowState(obj).setState(value)
+                msg = "State '%s' set" % value
+                history = IObjectHistory(obj)
+                history.addMessage(msg)
+            elif hasattr(obj, key):
+                setattr(obj, key, value)
+            items_changed += '%s=%s, ' % (key,value)
+        parent = self.getParent(row, site)
+        if hasattr(obj,'student_id'):
+            # Update mode: the student exists and we can get the student_id
+            parent.logger.info(
+                '%s - Student record updated: %s'
+                % (obj.student_id, items_changed))
+        else:
+            # Create mode: the student does not yet exist
+            parent.logger.info('Student data imported: %s' % items_changed)
+        return
+
+    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 = IStudentUpdateByRegNo
+            elif self.getLocator(row) == 'matric_number':
+                iface = IStudentUpdateByMatricNo
+        converter = IObjectConverter(iface)
+        errs, inv_errs, conv_dict =  converter.fromStringDict(
+            row, self.factory_name)
+        if row.has_key('reg_state') and \
+            not row['reg_state'] in IMPORTABLE_STATES:
+            if row['reg_state'] != '':
+                errs.append(('reg_state','not allowed'))
+            else:
+                errs.append(('reg_state','no value provided'))
+        return errs, inv_errs, conv_dict
+
+class StudentStudyCourseProcessor(BatchProcessor):
+    """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 Importer (update only)'
+    iface = IStudentStudyCourse
+    factory_name = 'waeup.StudentStudyCourse'
+
+    location_fields = []
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+            ['student_id','reg_number','matric_number'] + 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!")
+        # 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 getParent(self, row, site):
+        if not 'students' in site.keys():
+            return None
+        if 'student_id' in row.keys() and row['student_id']:
+            if row['student_id'] in site['students']:
+                student = site['students'][row['student_id']]
+                return student
+        elif 'reg_number' in row.keys() and 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 'matric_number' in row.keys() and 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 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 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):
+        """Update obj to the values given in row.
+        """
+        items_changed = ''
+        for key, value in row.items():
+            # Skip fields not declared in interface.
+            if hasattr(obj, key):
+                setattr(obj, key, value)
+                if key == 'certificate':
+                    value = value.code
+            items_changed += '%s=%s, ' % (key,value)
+        parent = self.getParent(row, site)
+        parent.__parent__.logger.info(
+            '%s - Study course updated: %s'
+            % (parent.student_id, items_changed))
+        # Update the students_catalog
+        notify(grok.ObjectModifiedEvent(obj.__parent__))
+        return
+
+    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)
+        # We have to check if current_level is in range of certificate.
+        # This is not done by the converter. This kind of conversion
+        # checking does only work if a combination of certificate and
+        # current_level is provided.
+        if conv_dict.has_key('certificate'):
+          certificate = conv_dict['certificate']
+          start_level = certificate.start_level
+          end_level = certificate.end_level
+          if conv_dict['current_level'] < start_level or \
+              conv_dict['current_level'] > end_level+120:
+              errs.append(('current_level','not in range'))
+        return errs, inv_errs, conv_dict
+
+class StudentStudyLevelProcessor(BatchProcessor):
+    """A batch processor for IStudentStudyLevel objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'studylevelimporter'
+    grok.name(util_name)
+
+    name = u'StudentStudyLevel Importer'
+    iface = IStudentStudyLevel
+    factory_name = 'waeup.StudentStudyLevel'
+
+    location_fields = []
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+            ['student_id','reg_number','matric_number','level'] + 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!")
+        if not 'level' in headerfields:
+            raise FatalCSVError(
+                "Need level for import!")
+        # 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 getParent(self, row, site):
+        if not 'students' in site.keys():
+            return None
+        if 'student_id' in row.keys() and row['student_id']:
+            if row['student_id'] in site['students']:
+                student = site['students'][row['student_id']]
+                return student['studycourse']
+        elif 'reg_number' in row.keys() and 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]['studycourse']
+        elif 'matric_number' in row.keys() and 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]['studycourse']
+        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 getEntry(self, row, site):
+        studycourse = self.getParent(row, site)
+        if studycourse is None:
+            return None
+        return studycourse.get(row['level'])
+
+    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.
+        """
+        converter = IObjectConverter(self.iface)
+        errs, inv_errs, conv_dict =  converter.fromStringDict(
+            row, self.factory_name)
+        # 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,700,10):
+                errs.append(('level','no valid integer'))
+        except ValueError:
+            errs.append(('level','no integer'))
+        return errs, inv_errs, conv_dict
+
+class CourseTicketProcessor(BatchProcessor):
+    """A batch processor for ICourseTicket objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'courseticketimporter'
+    grok.name(util_name)
+
+    name = u'CourseTicket Importer'
+    iface = ICourseTicket
+    factory_name = 'waeup.CourseTicket'
+
+    location_fields = []
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+            ['student_id','reg_number','matric_number','level','code'] + 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!")
+        if not 'level' in headerfields:
+            raise FatalCSVError(
+                "Need level for import!")
+        if not 'code' in headerfields:
+            raise FatalCSVError(
+                "Need code for import!")
+        # 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 getParent(self, row, site):
+        if not 'students' in site.keys():
+            return None
+        if 'student_id' in row.keys() and row['student_id']:
+            if row['student_id'] in site['students']:
+                student = site['students'][row['student_id']]
+                return student['studycourse'].get(row['level'])
+        elif 'reg_number' in row.keys() and 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]['studycourse'].get(row['level'])
+        elif 'matric_number' in row.keys() and 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]['studycourse'].get(row['level'])
+        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 getEntry(self, row, site):
+        level = self.getParent(row, site)
+        if level is None:
+            return None
+        return level.get(row['code'])
+
+    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
+        obj.credits = entries[0].credits
+        obj.passmark = entries[0].passmark
+        obj.semester = entries[0].semester
+        parent[row['code']] = obj
+        return
+
+    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)
+        # 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
+        return errs, inv_errs, conv_dict
+
+class StudentOnlinePaymentProcessor(BatchProcessor):
+    """A batch processor for IStudentOnlinePayment objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'paymentimporter'
+    grok.name(util_name)
+
+    name = u'Payment Importer'
+    iface = IStudentOnlinePayment
+    factory_name = 'waeup.StudentOnlinePayment'
+
+    location_fields = []
+
+    mode = None
+
+    @property
+    def available_fields(self):
+        return sorted(list(set(
+            ['student_id','reg_number','matric_number','p_id'] + 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!")
+        if not 'p_id' in headerfields:
+            raise FatalCSVError(
+                "Need p_id for import!")
+        # 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 getParent(self, row, site):
+        if not 'students' in site.keys():
+            return None
+        if 'student_id' in row.keys() and row['student_id']:
+            if row['student_id'] in site['students']:
+                student = site['students'][row['student_id']]
+                return student['payments']
+        elif 'reg_number' in row.keys() and 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]['payments']
+        elif 'matric_number' in row.keys() and 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]['payments']
+        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 getEntry(self, row, site):
+        payments = self.getParent(row, site)
+        if payments 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 = row['p_id'].strip('#')
+        if p_id.startswith('p'):
+            entry = payments.get(p_id)
+        else:
+            # For data migration from old SRP
+            entry = payments.get('p' + p_id[6:])
+        return entry
+
+    def addEntry(self, obj, row, site):
+        parent = self.getParent(row, site)
+        p_id = row['p_id'].strip('#')
+        if not p_id.startswith('p'):
+            # For data migration from old SRP
+            obj.p_id = 'p' + p_id[6:]
+            parent[obj.p_id] = obj
+        else:
+            parent[p_id] = obj
+        return
+
+    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)
+        # We have to check p_id.
+        p_id = row['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
+        else:
+            if not len(p_id) == 19:
+                errs.append(('p_id','invalid length'))
+                return errs, inv_errs, conv_dict
+        return errs, inv_errs, conv_dict
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser.py	(revision 7760)
@@ -0,0 +1,1925 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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
+from urllib import urlencode
+from time import time
+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.formlib.textwidgets import BytesDisplayWidget
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from waeup.sirp.accesscodes import (
+    invalidate_accesscode, get_access_code, create_accesscode)
+from waeup.sirp.accesscodes.workflow import USED
+from waeup.sirp.browser import (
+    SIRPPage, SIRPEditFormPage, SIRPAddFormPage, SIRPDisplayFormPage,
+    ContactAdminForm, SIRPForm)
+from waeup.sirp.browser.interfaces import ICaptchaManager
+from waeup.sirp.browser.breadcrumbs import Breadcrumb
+from waeup.sirp.browser.resources import datepicker, datatable, tabs, warning
+from waeup.sirp.browser.layout import jsaction, action, UtilityView
+from waeup.sirp.interfaces import (
+    ISIRPObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
+    ISIRPUtils, IUniversity)
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.widgets.datewidget import (
+    FriendlyDateWidget, FriendlyDateDisplayWidget,
+    FriendlyDatetimeDisplayWidget)
+from waeup.sirp.students.interfaces import (
+    IStudentsContainer, IStudent, IStudentClearance,
+    IStudentPersonal, IStudentBase, IStudentStudyCourse,
+    IStudentAccommodation, IStudentStudyLevel,
+    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
+    IStudentOnlinePayment, IBedTicket, IStudentsUtils, IStudentChangePassword
+    )
+from waeup.sirp.students.catalog import search
+from waeup.sirp.students.workflow import (ADMITTED, PAID,
+    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED)
+from waeup.sirp.students.studylevel import StudentStudyLevel, CourseTicket
+from waeup.sirp.students.vocabularies import StudyLevelSource
+from waeup.sirp.browser.resources import toggleall
+from waeup.sirp.hostels.hostel import NOT_OCCUPIED
+from waeup.sirp.utils.helpers import get_current_principal
+
+def write_log_message(view, message):
+    ob_class = view.__implemented__.__name__.replace('waeup.sirp.','')
+    view.context.getStudent().loggerInfo(ob_class, message)
+    return
+
+# 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.getStudent()))
+    fields_string = ' + '.join(changed_fields)
+    view.flash(_('Form has been saved.'))
+    if fields_string:
+        write_log_message(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
+
+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)
+    title = _('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):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        # There is no request attribute, thus we can only translate
+        # to the default (portal) language
+        return translate(self.context.level_title, 'waeup.sirp',
+            target_language=portal_language)
+
+class StudentsContainerPage(SIRPPage):
+    """The standard view for student containers.
+    """
+    grok.context(IStudentsContainer)
+    grok.name('index')
+    grok.require('waeup.viewStudentsContainer')
+    grok.template('containerpage')
+    label = _('Student Section')
+    search_button = _('Search')
+    pnav = 4
+
+    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
+        if self.searchtype == 'current_session':
+            self.searchterm = int(self.searchterm)
+        self.hitlist = search(query=self.searchterm,
+            searchtype=self.searchtype, view=self)
+        if not self.hitlist:
+            self.flash('No student found.')
+        return
+
+class StudentsContainerManagePage(SIRPPage):
+    """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 = _('Search')
+    remove_button = _('Remove selected')
+
+    def update(self, *args, **kw):
+        datatable.need()
+        toggleall.need()
+        warning.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
+        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(SIRPAddFormPage):
+    """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 StudentBaseDisplayFormPage(SIRPDisplayFormPage):
+    """ 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')
+    pnav = 4
+
+    @property
+    def label(self):
+        return _('${a}: Base Data',
+            mapping = {'a':self.context.display_fullname})
+
+    @property
+    def hasPassword(self):
+        if self.context.password:
+            return _('set')
+        return _('unset')
+
+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''):
+        self.form_fields.get('subject').field.default = subject
+        self.subject = subject
+        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()
+        sirp_utils = getUtility(ISIRPUtils)
+        success = sirp_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 StudentBaseManageFormPage(SIRPEditFormPage):
+    """ 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')
+    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
+
+    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 = self.wf_info.getManualTransitions()
+        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
+        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')
+        # ... and execute transition
+        if form.has_key('transition') and form['transition']:
+            transition_id = form['transition']
+            self.wf_info.fireTransition(transition_id)
+        fields_string = ' + '.join(changed_fields)
+        self.flash(_('Form has been saved.'))
+        if fields_string:
+            write_log_message(self, 'saved: % s' % fields_string)
+        return
+
+class StudentClearanceDisplayFormPage(SIRPDisplayFormPage):
+    """ Page to display student clearance data
+    """
+    grok.context(IStudent)
+    grok.name('view_clearance')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentClearance).omit('clearance_locked')
+    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
+    pnav = 4
+
+    @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.pdf')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentClearance).omit('clearance_locked')
+    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
+    prefix = 'form'
+
+    @property
+    def title(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        return translate(_('Clearance Data'), 'waeup.sirp',
+            target_language=portal_language)
+
+    @property
+    def label(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        return translate(_('Clearance Slip of '),
+            'waeup.sirp', target_language=portal_language) \
+            + ' %s' % self.context.display_fullname
+
+    def render(self):
+        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
+            self.request)
+        students_utils = getUtility(IStudentsUtils)
+        return students_utils.renderPDF(
+            self, 'clearance.pdf',
+            self.context.getStudent(), studentview)
+
+class StudentClearanceManageFormPage(SIRPEditFormPage):
+    """ Page to edit student clearance data
+    """
+    grok.context(IStudent)
+    grok.name('edit_clearance')
+    grok.require('waeup.manageStudent')
+    grok.template('clearanceeditpage')
+    form_fields = grok.AutoFields(IStudentClearance)
+    label = _('Manage clearance data')
+    pnav = 4
+    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
+
+    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 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(UtilityView, grok.View):
+    """ Reject clearance by clearance officers
+    """
+    grok.context(IStudent)
+    grok.name('reject_clearance')
+    grok.require('waeup.clearStudent')
+
+    def update(self):
+        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
+        args = {'subject':message}
+        self.redirect(self.url(self.context) +
+            '/contactstudent?%s' % urlencode(args))
+        return
+
+    def render(self):
+        return
+
+class StudentPersonalDisplayFormPage(SIRPDisplayFormPage):
+    """ 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
+    pnav = 4
+
+    @property
+    def label(self):
+        return _('${a}: Personal Data',
+            mapping = {'a':self.context.display_fullname})
+
+class StudentPersonalManageFormPage(SIRPEditFormPage):
+    """ Page to edit student clearance data
+    """
+    grok.context(IStudent)
+    grok.name('edit_personal')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentPersonal)
+    label = _('Manage personal data')
+    pnav = 4
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+class StudyCourseDisplayFormPage(SIRPDisplayFormPage):
+    """ Page to display the student study course data
+    """
+    grok.context(IStudentStudyCourse)
+    grok.name('index')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentStudyCourse)
+    grok.template('studycoursepage')
+    pnav = 4
+
+    @property
+    def label(self):
+        return _('${a}: Study Course',
+            mapping = {'a':self.context.__parent__.display_fullname})
+
+    @property
+    def current_mode(self):
+        if self.context.certificate is not None:
+            studymodes_dict = getUtility(ISIRPUtils).getStudyModesDict()
+            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
+
+class StudyCourseManageFormPage(SIRPEditFormPage):
+    """ Page to edit the student study course data
+    """
+    grok.context(IStudentStudyCourse)
+    grok.name('manage')
+    grok.require('waeup.manageStudent')
+    grok.template('studycoursemanagepage')
+    form_fields = grok.AutoFields(IStudentStudyCourse)
+    label = _('Manage study course')
+    pnav = 4
+    taboneactions = [_('Save'),_('Cancel')]
+    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
+    tabthreeactions = [_('Add study level')]
+
+    def update(self):
+        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):
+        msave(self, **data)
+        return
+
+    @property
+    def level_dict(self):
+        studylevelsource = StudyLevelSource().factory
+        for code in studylevelsource.getValues(self.context):
+            title = studylevelsource.getTitle(self.context, code)
+            yield(dict(code=code, title=title))
+
+    @action(_('Add study level'))
+    def addStudyLevel(self, **data):
+        level_code = self.request.form.get('addlevel', None)
+        studylevel = StudentStudyLevel()
+        studylevel.level = int(level_code)
+        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 form.has_key('val_id'):
+            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.redirect(self.url(self.context, u'@@manage')+'?tab2')
+        return
+
+class StudyLevelDisplayFormPage(SIRPDisplayFormPage):
+    """ Page to display student study levels
+    """
+    grok.context(IStudentStudyLevel)
+    grok.name('index')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentStudyLevel)
+    grok.template('studylevelpage')
+    pnav = 4
+
+    def update(self):
+        super(StudyLevelDisplayFormPage, self).update()
+        datatable.need()
+        return
+
+    @property
+    def label(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        lang = self.request.cookies.get('sirp.language', portal_language)
+        level_title = translate(self.context.level_title, 'waeup.sirp',
+            target_language=lang)
+        return _('${a}: Study Level ${b}', mapping = {
+            'a':self.context.getStudent().display_fullname,
+            'b':level_title})
+
+    @property
+    def total_credits(self):
+        total_credits = 0
+        for key, val in self.context.items():
+            total_credits += val.credits
+        return total_credits
+
+class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(IStudentStudyLevel)
+    grok.name('course_registration.pdf')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentStudyLevel)
+    prefix = 'form'
+
+    @property
+    def title(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        return translate(_('Level Data'), 'waeup.sirp',
+            target_language=portal_language)
+
+    @property
+    def content_title(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        return translate(_('Course List'), 'waeup.sirp',
+            target_language=portal_language)
+
+    @property
+    def label(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        lang = self.request.cookies.get('sirp.language', portal_language)
+        level_title = translate(self.context.level_title, 'waeup.sirp',
+            target_language=lang)
+        return translate(_('Course Registration Slip  '),
+            'waeup.sirp', target_language=portal_language) \
+            + ' %s' % level_title
+
+    def render(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        Sem = translate(_('Sem.'), 'waeup.sirp', target_language=portal_language)
+        Code = translate(_('Code'), 'waeup.sirp', target_language=portal_language)
+        Title = translate(_('Title'), 'waeup.sirp', target_language=portal_language)
+        Dept = translate(_('Dept.'), 'waeup.sirp', target_language=portal_language)
+        Faculty = translate(_('Faculty'), 'waeup.sirp', target_language=portal_language)
+        Cred = translate(_('Cred.'), 'waeup.sirp', target_language=portal_language)
+        Mand = translate(_('Mand.'), 'waeup.sirp', target_language=portal_language)
+        Score = translate(_('Score'), 'waeup.sirp', target_language=portal_language)
+        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
+            self.request)
+        students_utils = getUtility(IStudentsUtils)
+        tabledata = sorted(self.context.values(),
+            key=lambda value: str(value.semester) + value.code)
+        return students_utils.renderPDF(
+            self, 'course_registration.pdf',
+            self.context.getStudent(), studentview,
+            tableheader=[(Sem,'semester', 1.5),(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),('Auto', 'automatic', 1.5)
+                         ],
+            tabledata=tabledata)
+
+class StudyLevelManageFormPage(SIRPEditFormPage):
+    """ 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)
+    pnav = 4
+    taboneactions = [_('Save'),_('Cancel')]
+    tabtwoactions = [_('Add course ticket'),
+        _('Remove selected tickets'),_('Cancel')]
+
+    def update(self):
+        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()
+        return
+
+    @property
+    def label(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        lang = self.request.cookies.get('sirp.language', portal_language)
+        level_title = translate(self.context.level_title, 'waeup.sirp',
+            target_language=lang)
+        return _('Manage study level ${a}',
+            mapping = {'a':level_title})
+
+    @action(_('Save'), style='primary')
+    def save(self, **data):
+        msave(self, **data)
+        return
+
+    @action(_('Add course ticket'))
+    def addCourseTicket(self, **data):
+        self.redirect(self.url(self.context, '@@add'))
+
+    @jsaction(_('Remove selected tickets'))
+    def delCourseTicket(self, **data):
+        form = self.request.form
+        if form.has_key('val_id'):
+            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.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 str(self.context.__parent__.current_level) != self.context.__name__:
+            self.flash(_('This level does not correspond current level.'))
+        elif self.context.getStudent().state == REGISTERED:
+            IWorkflowInfo(self.context.getStudent()).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 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.getStudent().state == VALIDATED:
+            IWorkflowInfo(self.context.getStudent()).fireTransition('reset8')
+            message = _('Course list request has been annulled.')
+            self.flash(message)
+        elif self.context.getStudent().state == REGISTERED:
+            IWorkflowInfo(self.context.getStudent()).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.getStudent()) +
+            '/contactstudent?%s' % urlencode(args))
+        return
+
+    def render(self):
+        return
+
+class CourseTicketAddFormPage(SIRPAddFormPage):
+    """Add a course ticket.
+    """
+    grok.context(IStudentStudyLevel)
+    grok.name('add')
+    grok.require('waeup.manageStudent')
+    label = _('Add course ticket')
+    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
+        'grade', 'score', 'automatic', 'carry_over')
+    pnav = 4
+
+    @action(_('Add course ticket'))
+    def addCourseTicket(self, **data):
+        ticket = CourseTicket()
+        course = data['course']
+        ticket.automatic = False
+        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
+        try:
+            self.context.addCourseTicket(ticket)
+        except KeyError:
+            self.flash(_('The ticket exists.'))
+            return
+        self.flash(_('Successfully added ${a}.',
+            mapping = {'a':ticket.code}))
+        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
+        return
+
+    @action(_('Cancel'))
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+class CourseTicketDisplayFormPage(SIRPDisplayFormPage):
+    """ 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.getStudent().display_fullname,
+            'b':self.context.code})
+
+class CourseTicketManageFormPage(SIRPEditFormPage):
+    """ Page to manage course tickets
+    """
+    grok.context(ICourseTicket)
+    grok.name('manage')
+    grok.require('waeup.manageStudent')
+    form_fields = grok.AutoFields(ICourseTicket)
+    grok.template('courseticketmanagepage')
+    pnav = 4
+
+    @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(SIRPEditFormPage):
+    """ 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.payStudent')
+    form_fields = grok.AutoFields(IStudentPaymentsContainer)
+    grok.template('paymentsmanagepage')
+    pnav = 4
+
+    def unremovable(self, ticket):
+        usertype = getattr(self.request.principal, 'user_type', None)
+        if not usertype:
+            return False
+        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 form.has_key('val_id'):
+            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
+            if not self.unremovable(self.context[id]):
+                del self.context[id]
+                deleted.append(id)
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping = {'a': ', '.join(deleted)}))
+            write_log_message(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(SIRPAddFormPage):
+    """ Page to add an online payment ticket
+    """
+    grok.context(IStudentPaymentsContainer)
+    grok.name('addop')
+    grok.require('waeup.payStudent')
+    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
+        'p_category')
+    label = _('Add online payment')
+    pnav = 4
+
+    @action(_('Create ticket'), style='primary')
+    def createTicket(self, **data):
+        p_category = data['p_category']
+        student = self.context.__parent__
+        if p_category == 'bed_allocation' and student[
+            'studycourse'].current_session != grok.getSite()[
+            'configuration'].accommodation_session:
+                self.flash(
+                    _('Your current session does not match ' + \
+                    'accommodation session.'))
+                self.redirect(self.url(self.context))
+                return
+        students_utils = getUtility(IStudentsUtils)
+        pay_details  = students_utils.getPaymentDetails(
+            p_category,student)
+        if pay_details['error']:
+            self.flash(pay_details['error'])
+            self.redirect(self.url(self.context))
+            return
+        p_item = pay_details['p_item']
+        p_session = pay_details['p_session']
+        for key in self.context.keys():
+            ticket = self.context[key]
+            if ticket.p_state == 'paid' and\
+               ticket.p_category == p_category and \
+               ticket.p_item == p_item and \
+               ticket.p_session == p_session:
+                  self.flash(
+                      _('This type of payment has already been made.'))
+                  self.redirect(self.url(self.context))
+                  return
+        payment = createObject(u'waeup.StudentOnlinePayment')
+        self.applyData(payment, **data)
+        timestamp = "%d" % int(time()*1000)
+        payment.p_id = "p%s" % timestamp
+        payment.p_item = p_item
+        payment.p_session = p_session
+        payment.amount_auth = pay_details['amount']
+        payment.surcharge_1 = pay_details['surcharge_1']
+        payment.surcharge_2 = pay_details['surcharge_2']
+        payment.surcharge_3 = pay_details['surcharge_3']
+        self.context[payment.p_id] = payment
+        self.flash(_('Payment ticket created.'))
+        self.redirect(self.url(self.context))
+        return
+
+class OnlinePaymentDisplayFormPage(SIRPDisplayFormPage):
+    """ Page to view an online payment ticket
+    """
+    grok.context(IStudentOnlinePayment)
+    grok.name('index')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentOnlinePayment)
+    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.getStudent().display_fullname,
+            'b':self.context.p_id})
+
+class OnlinePaymentCallbackPage(UtilityView, grok.View):
+    """ Callback view
+    """
+    grok.context(IStudentOnlinePayment)
+    grok.name('callback')
+    grok.require('waeup.payStudent')
+
+    # This update method simulates a valid callback und must be
+    # specified in the customization package. The parameters must be taken
+    # from the incoming request.
+    def update(self):
+        if self.context.p_state == 'paid':
+            self.flash(_('This ticket has already been paid.'))
+            return
+        student = self.context.getStudent()
+        write_log_message(self,'valid callback: %s' % self.context.p_id)
+        self.context.r_amount_approved = self.context.amount_auth
+        self.context.r_card_num = u'0000'
+        self.context.r_code = u'00'
+        self.context.p_state = 'paid'
+        self.context.payment_date = datetime.now()
+        if self.context.p_category == 'clearance':
+            # Create CLR access code
+            pin, error = create_accesscode('CLR',0,student.student_id)
+            if error:
+                self.flash(_('Valid callback received. ${a}',
+                    mapping = {'a':error}))
+                return
+            self.context.ac = pin
+        elif self.context.p_category == 'schoolfee':
+            # Create SFE access code
+            pin, error = create_accesscode('SFE',0,student.student_id)
+            if error:
+                self.flash(_('Valid callback received. ${a}',
+                    mapping = {'a':error}))
+                return
+            self.context.ac = pin
+        elif self.context.p_category == 'bed_allocation':
+            # Create HOS access code
+            pin, error = create_accesscode('HOS',0,student.student_id)
+            if error:
+                self.flash(_('Valid callback received. ${a}',
+                    mapping = {'a':error}))
+                return
+            self.context.ac = pin
+        self.flash(_('Valid callback received.'))
+        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(IStudentOnlinePayment)
+    grok.name('payment_receipt.pdf')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentOnlinePayment)
+    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
+    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
+    prefix = 'form'
+    title = 'Payment Data'
+
+    @property
+    def label(self):
+        return 'Online Payment Receipt %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 = StudentBaseDisplayFormPage(self.context.getStudent(),
+            self.request)
+        students_utils = getUtility(IStudentsUtils)
+        return students_utils.renderPDF(self, 'payment_receipt.pdf',
+            self.context.getStudent(), studentview)
+
+
+class AccommodationManageFormPage(SIRPEditFormPage):
+    """ 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 form.has_key('val_id'):
+            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)}))
+            write_log_message(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(SIRPPage):
+    """ 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 = ''
+
+    def update(self, SUBMIT=None):
+        student = self.context.getStudent()
+        students_utils = getUtility(IStudentsUtils)
+        acc_details  = students_utils.getAccommodationDetails(student)
+        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
+        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
+        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 bee manually allocated use this bed
+            bed = [entry for entry in entries][0]
+        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)
+                bed.bookBed(student.student_id)
+            else:
+                self.flash(_('There is no free bed in your category ${a}.',
+                    mapping = {'a':acc_details['bt']}))
+                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 exception, but the owner might be different
+            if not invalidate_accesscode(
+                pin,comment,self.context.getStudent().student_id):
+                self.flash(_('You are not the owner of this access code.'))
+                return
+        # Create bed ticket
+        bedticket = createObject(u'waeup.BedTicket')
+        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.getBedCoordinates()[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(ISIRPUtils).PORTAL_LANGUAGE
+        bedticket.bed_coordinates = translate(
+            bc, 'waeup.sirp',target_language=portal_language)
+        key = str(acc_details['booking_session'])
+        self.context[key] = bedticket
+        self.flash(_('Bed ticket created and bed booked: ${a}',
+            mapping = {'a':bedticket.bed_coordinates}))
+        self.redirect(self.url(self.context))
+        return
+
+class BedTicketDisplayFormPage(SIRPDisplayFormPage):
+    """ Page to display bed tickets
+    """
+    grok.context(IBedTicket)
+    grok.name('index')
+    grok.require('waeup.handleAccommodation')
+    form_fields = grok.AutoFields(IBedTicket)
+    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.pdf')
+    grok.require('waeup.handleAccommodation')
+    form_fields = grok.AutoFields(IBedTicket)
+    form_fields['booking_date'].custom_widget = FriendlyDateDisplayWidget('le')
+    prefix = 'form'
+
+    @property
+    def title(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        return translate(_('Bed Allocation Data'), 'waeup.sirp',
+            target_language=portal_language)
+
+    @property
+    def label(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        return translate(_('Bed Allocation: '),
+            'waeup.sirp', target_language=portal_language) \
+            + ' %s' % self.context.bed_coordinates
+
+    def render(self):
+        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
+            self.request)
+        students_utils = getUtility(IStudentsUtils)
+        return students_utils.renderPDF(
+            self, 'bed_allocation.pdf',
+            self.context.getStudent(), studentview)
+
+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.getStudent()
+        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.getBedCoordinates()[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(ISIRPUtils).PORTAL_LANGUAGE
+        self.context.bed_coordinates = translate(
+            bc, 'waeup.sirp',target_language=portal_language)
+        self.flash(_('Student relocated: ${a}',
+            mapping = {'a':self.context.bed_coordinates}))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class StudentHistoryPage(SIRPPage):
+    """ 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(SIRPEditFormPage):
+    """ 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(SIRPEditFormPage):
+    """ 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)
+                write_log_message(self, 'saved: password')
+                self.flash(_('Password changed.'))
+            else:
+                self.flash( ' '.join(errors))
+        return
+
+class StudentFilesUploadPage(SIRPPage):
+    """ 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):
+        if self.context.getStudent().state != ADMITTED:
+            emit_lock_message(self)
+            return
+        super(StudentFilesUploadPage, self).update()
+        return
+
+class StartClearancePage(SIRPPage):
+    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')
+
+    @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
+        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
+        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)
+        # and fire transition start_clearance
+        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_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')
+    form_fields = grok.AutoFields(
+        IStudentClearance).omit('clearance_locked')
+    label = _('Edit clearance data')
+    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
+
+    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.'))
+        self.redirect(self.url(self.context,'request_clearance'))
+        return
+
+class RequestClearancePage(SIRPPage):
+    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')
+
+    def update(self, SUBMIT=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
+        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
+        if self.context.clr_code != pin:
+            self.flash(_("This isn't your CLR access code."))
+            return
+        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 != CLEARANCE:
+            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 StartCourseRegistrationPage(SIRPPage):
+    grok.context(IStudentStudyCourse)
+    grok.name('start_course_registration')
+    grok.require('waeup.handleStudent')
+    grok.template('enterpin')
+    label = _('Start course registration')
+    ac_prefix = 'SFE'
+    notice = ''
+    pnav = 4
+    buttonname = _('Start course registration now')
+
+    def update(self, SUBMIT=None):
+        if not self.context.getStudent().state in (CLEARED,RETURNING):
+            self.flash(_("Wrong state"))
+            self.redirect(self.url(self.context))
+            return
+        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
+        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)
+        # and fire transition start_clearance
+        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.getStudent().student_id):
+                self.flash(_('You are not the owner of this access code.'))
+                return
+        if self.context.getStudent().state == CLEARED:
+            IWorkflowInfo(self.context.getStudent()).fireTransition(
+                'pay_first_school_fee')
+        elif self.context.getStudent().state == RETURNING:
+            IWorkflowInfo(self.context.getStudent()).fireTransition(
+                'pay_school_fee')
+        self.flash(_('Course registration has been started.'))
+        self.redirect(self.url(self.context))
+        return
+
+class AddStudyLevelFormPage(SIRPEditFormPage):
+    """ 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 self.context.getStudent().state != PAID:
+            emit_lock_message(self)
+            return
+        super(AddStudyLevelFormPage, self).update()
+        return
+
+    @action(_('Create course list now'), style='primary')
+    def addStudyLevel(self, **data):
+        studylevel = 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.'))
+        self.redirect(self.url(self.context))
+        return
+
+class StudyLevelEditFormPage(SIRPEditFormPage):
+    """ Page to edit the student study level data by students
+    """
+    grok.context(IStudentStudyLevel)
+    grok.name('edit')
+    grok.require('waeup.handleStudent')
+    grok.template('studyleveleditpage')
+    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
+        'level_session', 'level_verdict')
+    pnav = 4
+
+    def update(self):
+        if self.context.getStudent().state != PAID:
+            emit_lock_message(self)
+            return
+        super(StudyLevelEditFormPage, self).update()
+        datatable.need()
+        warning.need()
+        return
+
+    @property
+    def label(self):
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        lang = self.request.cookies.get('sirp.language', portal_language)
+        level_title = translate(self.context.level_title, 'waeup.sirp',
+            target_language=lang)
+        return _('Add and remove course tickets of study level ${a}',
+            mapping = {'a':level_title})
+
+    @property
+    def total_credits(self):
+        total_credits = 0
+        for key, val in self.context.items():
+            total_credits += val.credits
+        return total_credits
+
+    @action(_('Add course ticket'))
+    def addCourseTicket(self, **data):
+        self.redirect(self.url(self.context, 'ctadd'))
+
+    @jsaction(_('Remove selected tickets'))
+    def delCourseTicket(self, **data):
+        form = self.request.form
+        if form.has_key('val_id'):
+            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 not self.context[id].mandatory:
+                del self.context[id]
+                deleted.append(id)
+        if len(deleted):
+            self.flash(_('Successfully removed: ${a}',
+                mapping = {'a':', '.join(deleted)}))
+        self.redirect(self.url(self.context, u'@@edit'))
+        return
+
+    @action(_('Register course list'), style='primary')
+    def RegisterCourses(self, **data):
+        IWorkflowInfo(self.context.getStudent()).fireTransition(
+            'register_courses')
+        self.flash(_('Course list has been registered.'))
+        self.redirect(self.url(self.context))
+        return
+
+class CourseTicketAddFormPage2(CourseTicketAddFormPage):
+    """Add a course ticket by student.
+    """
+    grok.name('ctadd')
+    grok.require('waeup.handleStudent')
+    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
+        'grade', 'score', 'mandatory', 'automatic', 'carry_over')
+
+    def update(self):
+        if self.context.getStudent().state != PAID:
+            emit_lock_message(self)
+            return
+        super(CourseTicketAddFormPage2, self).update()
+        return
+
+    @action(_('Add course ticket'))
+    def addCourseTicket(self, **data):
+        # Safety belt
+        if self.context.getStudent().state != PAID:
+            return
+        ticket = CourseTicket()
+        course = data['course']
+        for name in ['code', 'title', 'credits', 'passmark', 'semester']:
+            setattr(ticket, name, getattr(course, name))
+        ticket.automatic = False
+        try:
+            self.context.addCourseTicket(ticket)
+        except KeyError:
+            self.flash(_('The ticket exists.'))
+            return
+        self.flash(_('Successfully added ${a}.',
+            mapping = {'a':ticket.code}))
+        self.redirect(self.url(self.context, u'@@edit'))
+        return
+
+class ChangePasswordRequestPage(SIRPForm):
+    """Captcha'd page for students to request a password change.
+    """
+    grok.context(IUniversity)
+    grok.name('changepw')
+    grok.require('waeup.Anonymous')
+    grok.template('changepw')
+    label = _('Change my password')
+    form_fields = grok.AutoFields(IStudentChangePassword)
+
+    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
+
+    @action(_('Get new login credentials'), 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
+        cat = queryUtility(ICatalog, name='students_catalog')
+        reg_number = data['reg_number']
+        email = data['email']
+        results = cat.searchResults(
+            reg_number=(reg_number, reg_number),
+            email=(email,email))
+        if len(results) == 0:
+            self.flash(_('No student record found.'))
+            return
+        student = list(results)[0]
+        # Change password
+        sirp_utils = getUtility(ISIRPUtils)
+        pwd = sirp_utils.genPassword()
+        IUserAccount(student).setPassword(pwd)
+        # Send email with new redentials
+        msg = _('You have successfully changed your password for the')
+        login_url = self.url(grok.getSite(), 'login')
+        success = sirp_utils.sendCredentials(
+            IUserAccount(student),pwd,login_url,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.'))
+        return
+
+class SetPasswordPage(SIRPPage):
+    grok.context(ISIRPObject)
+    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
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/accommodationmanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/accommodationmanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/accommodationmanagepage.pt	(revision 7760)
@@ -0,0 +1,36 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.sirp" 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/bed_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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/basemanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/basemanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/basemanagepage.pt	(revision 7760)
@@ -0,0 +1,58 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.sirp" enctype="multipart/form-data">
+
+  <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>
+          <tr>
+            <td class="fieldname" i18n:translate="">Registration 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="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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/basepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/basepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/basepage.pt	(revision 7760)
@@ -0,0 +1,31 @@
+<table i18n:domain="waeup.sirp" 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/change_password.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/change_password.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/change_password.pt	(revision 7760)
@@ -0,0 +1,29 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.sirp" 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/changepw.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/changepw.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/changepw.pt	(revision 7760)
@@ -0,0 +1,24 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.sirp" 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>
+
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/clearanceeditpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/clearanceeditpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/clearanceeditpage.pt	(revision 7760)
@@ -0,0 +1,33 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.sirp" 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/containermanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/containermanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/containermanagepage.pt	(revision 7760)
@@ -0,0 +1,66 @@
+<form method="POST" i18n:domain="waeup.sirp">
+<br />
+  <input class="btn primary" type="submit" name="search"
+    tal:attributes="value view/search_button" />
+  <span i18n:translate="">for students</span>
+
+  <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>
+  </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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/containerpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/containerpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/containerpage.pt	(revision 7760)
@@ -0,0 +1,54 @@
+<form method="POST" i18n:domain="waeup.sirp">
+<br />
+  <input class="btn primary" type="submit" name="search"
+    tal:attributes="value view/search_button" />
+  <span i18n:translate="">for students</span>
+
+  <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>
+  </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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/courseticketmanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/courseticketmanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/courseticketmanagepage.pt	(revision 7760)
@@ -0,0 +1,62 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      i18n:domain="waeup.sirp" enctype="multipart/form-data">
+
+  <table class="form-table">
+    <tbody>
+      <tr>
+        <td class="fieldname" i18n:translate="">
+            Course Title:
+        </td>
+        <td>
+            <span tal:replace="context/title">TITLE</span>
+        </td>
+      </tr>
+      <tr>
+        <td class="fieldname" i18n:translate="">
+            Faculty:
+        </td>
+        <td>
+            <span tal:replace="context/fcode">FACULTY</span>
+        </td>
+      </tr>
+      <tr>
+        <td class="fieldname" i18n:translate="">
+            Department:
+        </td>
+        <td>
+            <span tal:replace="context/dcode">DEPARTMENT</span>
+        </td>
+      </tr>
+      <tr>
+        <td class="fieldname" i18n:translate="">
+            Semester:
+        </td>
+        <td>
+            <span tal:replace="context/semester">SEMESTER</span>
+        </td>
+      </tr>
+      <tr>
+        <td class="fieldname" i18n:translate="">
+            Credits:
+        </td>
+        <td>
+            <span tal:replace="context/credits">CREDITS</span>
+        </td>
+      </tr>
+      <tr>
+        <td class="fieldname" i18n:translate="">
+            Passmark:
+        </td>
+        <td>
+            <span tal:replace="context/passmark">PASSMARK</span>
+        </td>
+      </tr>
+      <tal:widgets content="structure provider:widgets" />
+    </tbody>
+  </table>
+  <div tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/courseticketpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/courseticketpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/courseticketpage.pt	(revision 7760)
@@ -0,0 +1,62 @@
+<table class="form-table" i18n:domain="waeup.sirp">
+  <tbody>
+    <tr>
+      <td class="fieldname">
+          Course Title:
+      </td>
+      <td>
+          <span tal:replace="context/title">TITLE</span>
+      </td>
+    </tr>
+    <tr>
+      <td class="fieldname">
+          Department:
+      </td>
+      <td>
+          <span tal:replace="context/dcode">DEPARTMENT</span>
+      </td>
+    </tr>
+    <tr>
+      <td class="fieldname">
+          Faculty:
+      </td>
+      <td>
+          <span tal:replace="context/fcode">FACULTY</span>
+      </td>
+    </tr>
+    <tr>
+      <td class="fieldname">
+          Semester:
+      </td>
+      <td>
+          <span tal:replace="context/semester">SEMESTER</span>
+      </td>
+    </tr>
+    <tr>
+      <td class="fieldname">
+          Credits:
+      </td>
+      <td>
+          <span tal:replace="context/credits">CREDITS</span>
+      </td>
+    </tr>
+    <tr>
+      <td class="fieldname">
+          Passmark:
+      </td>
+      <td>
+          <span tal:replace="context/passmark">PASSMARK</span>
+      </td>
+    </tr>
+    <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>
+  </tbody>
+</table>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/enterpin.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/enterpin.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/enterpin.pt	(revision 7760)
@@ -0,0 +1,26 @@
+<form method="POST" i18n:domain="waeup.sirp">
+  <table class="form-table">
+    <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 colspan="2">
+          <input type="submit" name="SUBMIT" class="btn primary"
+                 tal:attributes="value view/buttonname" />
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</form>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/filedisplay.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/filedisplay.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/filedisplay.pt	(revision 7760)
@@ -0,0 +1,14 @@
+<tr tal:condition = "viewlet/file_exists">
+  <td class="fieldname">
+    <span tal:replace="viewlet/label">FILENAME</span>:
+  </td>
+  <td>
+    <span class="widget">
+      <a tal:attributes="href viewlet/download_name"
+         tal:content="viewlet/download_name"
+         target="image">
+        LINK
+      </a>
+    </span>
+  </td>
+</tr>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/filesuploadpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/filesuploadpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/filesuploadpage.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/fileupload.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/fileupload.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/fileupload.pt	(revision 7760)
@@ -0,0 +1,34 @@
+<tr i18n:domain="waeup.sirp">
+  <td class="fieldname">
+    <span tal:replace="viewlet/label">FILENAME</span>:
+  </td>
+  <td>
+    <span class="widget">
+      <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>
+    </span>
+    <br />
+  </td>
+</tr>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/imagedisplay.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/imagedisplay.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/imagedisplay.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/imageupload.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/imageupload.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/imageupload.pt	(revision 7760)
@@ -0,0 +1,26 @@
+<tr i18n:domain="waeup.sirp">
+  <td class="fieldname">
+    <span tal:replace="viewlet/label">FILENAME</span>:
+  </td>
+  <td>
+    <span class="widget">
+      <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="Upload" class="btn primary" />
+      <span i18n:translate="">
+        Max. file size:
+      </span>
+      <span tal:replace="viewlet/max_upload_size">10 KB</span>
+    </span>
+  </td>
+</tr>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/mydatadropdowntabs.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/mydatadropdowntabs.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/mydatadropdowntabs.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/paymentsmanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/paymentsmanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/paymentsmanagepage.pt	(revision 7760)
@@ -0,0 +1,43 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.sirp" 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="">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 tal:content ="cl/category">CATEGORY</td>
+      <td tal:content ="cl/p_item">ITEM</td>
+      <td tal:content ="cl/p_session">SESSION</td>
+      <td tal:content ="cl/state">STATE</td>
+      </tr>
+    </tbody>
+  </table>
+
+  <div tal:condition="view/availableActions">
+    <span tal:repeat="action view/actions"
+          tal:omit-tag="">
+      <input tal:replace="structure action/render"/>
+    </span>
+  </div>
+</form>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/setpassword.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/setpassword.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/setpassword.pt	(revision 7760)
@@ -0,0 +1,46 @@
+<form i18n:domain="waeup.sirp" 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.sirp" >
+  <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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studenthistory.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studenthistory.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studenthistory.pt	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studentpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studentpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studentpage.pt	(revision 7760)
@@ -0,0 +1,24 @@
+<table 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>
+  </tbody>
+  <tfoot>
+    <tr>
+      <td colspan="2" class="align-right">
+        <input tal:repeat="action view/actions"
+               tal:replace="structure action/render" />
+      </td>
+    </tr>
+  </tfoot>
+</table>
+
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studycoursemanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studycoursemanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studycoursemanagepage.pt	(revision 7760)
@@ -0,0 +1,84 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.sirp" 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">
+          <option tal:attributes="value level/code" tal:repeat="level view/level_dict">
+            <span tal:replace="level/title">LEVELTITLE
+          </span>
+          </option>
+        </select>
+      </div>
+    </div>
+  </div>
+</form>  
+
+
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studycoursepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studycoursepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studycoursepage.pt	(revision 7760)
@@ -0,0 +1,74 @@
+<table class="form-table" i18n:domain="waeup.sirp">
+  <thead>
+  </thead>
+  <tbody>
+    <tal:block repeat="widget view/widgets">
+      <tr>
+        <td class="filedname">
+          <span tal:content="widget/label">label</span>:
+        </td>
+        <td tal:on-error="default">
+          <div class="widget" tal:content="structure widget">
+            <input type="text" />
+          </div>
+        </td>
+      </tr>
+    </tal:block>
+      <tr>
+        <td i18n:translate="">
+            Study Mode:
+        </td>
+        <td class="field">
+          <div class="widget" tal:content="view/current_mode">
+            <input type="text" />
+          </div>
+        </td>
+      </tr>
+      <tr tal:condition="python: view.department is not None">
+        <td i18n:translate="">
+            Department:
+        </td>
+        <td class="field">
+          <a tal:attributes="href python: view.url(view.department)" class="widget"
+               tal:content="view/department/longtitle">
+            DEPARTMENT
+          </a>
+        </td>
+      </tr>
+      <tr tal:condition="python: view.faculty is not None">
+        <td i18n:translate="">
+            Faculty:
+        </td>
+        <td class="field">
+          <a tal:attributes="href python: view.url(view.faculty)" class="widget"
+               tal:content="view/faculty/longtitle">
+            FACULTY
+          </a>
+        </td>
+      </tr>
+  </tbody>
+</table>
+
+<h3 i18n:domain="waeup.sirp" i18n:translate="">Study Levels (Course Lists)</h3>
+
+<table i18n:domain="waeup.sirp">
+  <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.sirp" i18n:translate="">
+  There are no levels registered yet.
+</div>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studyleveladdpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studyleveladdpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studyleveladdpage.pt	(revision 7760)
@@ -0,0 +1,15 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.sirp" 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studyleveleditpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studyleveleditpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studyleveleditpage.pt	(revision 7760)
@@ -0,0 +1,53 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.sirp" enctype="multipart/form-data">
+
+  <h3 i18n:translate="">
+    Course Tickets
+    (Total Credits: <span tal:replace="view/total_credits">TOTAL</span>)
+  </h3>
+  <table class="display dataTableManage">
+  <thead>
+    <tr>
+      <th>&nbsp;</th>
+      <th i18n:translate="">Sem.</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 context/values" class="gradeC">
+       <td>
+        <input type="checkbox" name="val_id"
+  		  tal:attributes="value value/__name__"
+        tal:condition="not: value/mandatory" />
+      </td>
+      <td tal:content="value/semester">SEMESTER</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">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>
+
+</form>  
+
+
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studylevelmanagepage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studylevelmanagepage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studylevelmanagepage.pt	(revision 7760)
@@ -0,0 +1,82 @@
+<form action="." tal:attributes="action request/URL" method="POST"
+      i18n:domain="waeup.sirp" 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="">Sem.</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="">Mand.</th>
+          <th i18n:translate="">Score</th>
+          <th i18n:translate="">CO</th>
+          <th i18n:translate="">Auto</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="value context/values" class="gradeC">
+          <td>
+            <input type="checkbox" name="val_id"
+      		  tal:attributes="value value/__name__" />
+          </td>
+          <td tal:content="value/semester">SEMESTER</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">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>
+
+    </div>
+  </div>
+</form>  
+
+
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studylevelpage.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studylevelpage.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/browser_templates/studylevelpage.pt	(revision 7760)
@@ -0,0 +1,67 @@
+<table class="form-table" i18n:domain="waeup.sirp">
+  <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 class="widget" tal:content="structure widget">
+            <input type="text" />
+          </div>
+        </td>
+      </tr>
+    </tal:block>
+    <tr>
+      <td i18n:translate="">
+        Total Credits:
+      </td>
+      <td>
+        <span tal:replace="view/total_credits">TOTAL</span>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+<h3 i18n:domain="waeup.sirp" i18n:translate="">Course Tickets</h3>
+
+<table class="display dataTable" i18n:domain="waeup.sirp">
+  <thead>
+    <tr>
+      <th i18n:translate="">Sem.</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="">Mand.</th>
+      <th i18n:translate="">Score</th>
+      <th i18n:translate="">CO</th>
+      <th i18n:translate="">Auto</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="value context/values" class="gradeC">
+      <td tal:content="value/semester">SEMESTER</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">SCORE</td>
+      <td tal:content="value/carry_over">CO</td>
+      <td tal:content="value/automatic">AUTO</td>
+    </tr>
+  </tbody>
+</table>
+
+
+<div i18n:domain="waeup.sirp"
+  tal:condition="python: not len(context.keys())" i18n:translate="">
+  The're no tickets added yet.
+</div>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/catalog.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/catalog.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/catalog.py	(revision 7760)
@@ -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
+##
+"""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.sirp.interfaces import (
+    IUniversity, IQueryResultItem, academic_sessions_vocab)
+from waeup.sirp.students.interfaces import (IStudent, ICourseTicket,
+    IStudentOnlinePayment)
+from waeup.sirp.university.vocabularies import course_levels
+
+class StudentIndexes(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_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
+        try:
+            current_level = course_levels.getTerm(
+                context['studycourse'].current_level).title
+        except LookupError:
+            current_level = None
+        self.current_level = 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
+
+def search(query=None, searchtype=None, view=None):
+    hitlist = []
+    if searchtype in ('fullname',):
+        results = Query().searchResults(
+            Text(('students_catalog', searchtype), query))
+    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='getLevel')
+    session = index.Field(attribute='getLevelSession')
+    code = index.Field(attribute='code')
+
+class StudentPaymentIndexes(grok.Indexes):
+    """A catalog for payments.
+    """
+    grok.site(IUniversity)
+    grok.name('payments_catalog')
+    grok.context(IStudentOnlinePayment)
+
+    p_session = index.Field(attribute='p_session')
+    p_category = index.Field(attribute='p_category')
+    p_item = index.Field(attribute='p_item')
+    p_state = index.Field(attribute='p_state')
+    r_pay_reference = index.Text(attribute='r_pay_reference')
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/container.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/container.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/container.py	(revision 7760)
@@ -0,0 +1,56 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 waeup.sirp.students.interfaces import (
+    IStudentsContainer, IStudent)
+from waeup.sirp.utils.logger import Logger
+
+class StudentsContainer(grok.Container, Logger):
+    """
+    The node containing the student models
+    """
+
+    grok.implements(IStudentsContainer)
+
+    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
+
+    logger_name = 'waeup.sirp.${sitename}.students'
+    logger_filename = 'students.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
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/dynamicroles.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/dynamicroles.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/dynamicroles.py	(revision 7760)
@@ -0,0 +1,128 @@
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public 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.sirp.students.interfaces import IStudent
+
+# All components in here have the same context: Student instances
+grok.context(IStudent)
+
+class StudentPrincipalRoleManager(AnnotationPrincipalRoleManager,
+                                    grok.Adapter):
+    grok.provides(IPrincipalRoleManager)
+
+    #: 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',
+        }
+
+    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.
+        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 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 '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 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/interfaces.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/interfaces.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/interfaces.py	(revision 7760)
@@ -0,0 +1,493 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.interfaces import (
+    ISIRPObject, academic_sessions_vocab, validate_email, ISIRPUtils)
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.schema import TextLineChoice
+from waeup.sirp.university.vocabularies import CourseSource, StudyModeSource
+from waeup.sirp.students.vocabularies import (
+  CertificateSource, StudyLevelSource,
+  contextual_reg_num_source, contextual_mat_num_source,
+  GenderSource, nats_vocab,
+  )
+from waeup.sirp.payments.interfaces import IPaymentsContainer, IOnlinePayment
+
+# 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).getVerdictsDict()
+        return verdicts_dict.keys()
+
+    def getToken(self, context, value):
+        return value
+
+    def getTitle(self, context, value):
+        verdicts_dict = getUtility(IStudentsUtils).getVerdictsDict()
+        return verdicts_dict[value]
+
+
+class IStudentsUtils(Interface):
+    """A collection of methods which are subject to customization.
+
+    """
+    def getPaymentDetails(category, student):
+        """Get the payment dates 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 renderPDF(view, subject='', filename='slip.pdf',):
+        """Render pdf slips for various pages.
+
+        """
+
+class IStudentsContainer(ISIRPObject):
+    """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.
+        """
+
+class IStudentNavigation(ISIRPObject):
+    """Interface needed for student navigation.
+
+    """
+    def getStudent():
+        """Return student object.
+
+        """
+
+class IStudentBase(ISIRPObject):
+    """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')
+    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')
+    current_session = Attribute('The current session of the student')
+    current_mode = Attribute('The current mode of the student')
+    fullname = Attribute('All name parts separated by hyphens')
+    display_fullname = Attribute('The fullname of an applicant')
+
+    def loggerInfo(ob_class, comment):
+        """Adds an INFO message to the log file.
+
+        """
+
+    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(),
+        default = u'm',
+        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'),
+        #default = u'',
+        required = False,
+        readonly = False,
+        source = contextual_mat_num_source,
+        )
+
+    adm_code = schema.TextLine(
+        title = _(u'PWD Activation Code'),
+        default = u'',
+        required = False,
+        readonly = True,
+        )
+
+    email = schema.ASCIILine(
+        title = _(u'Email'),
+        required = False,
+        constraint=validate_email,
+        )
+    phone = schema.TextLine(
+        title = _(u'Phone'),
+        description = u'',
+        required = False,
+        )
+
+class IStudentClearance(ISIRPObject):
+    """Representation of student clearance data.
+
+    """
+    date_of_birth = schema.Date(
+        title = _(u'Date of Birth'),
+        required = True,
+        )
+
+    clearance_locked = schema.Bool(
+        title = _(u'Clearance form locked'),
+        default = False,
+        )
+
+    clr_code = schema.TextLine(
+        title = _(u'CLR Activation Code'),
+        default = u'',
+        required = False,
+        readonly = True,
+        )
+
+    nationality = schema.Choice(
+        source = nats_vocab,
+        title = _(u'Nationality'),
+        default = 'nigeria',
+        required = True,
+        )
+
+class IStudentPersonal(ISIRPObject):
+    """Representation of student personal data.
+
+    """
+    perm_address = schema.Text(
+        title = _(u'Permanent Address'),
+        required = False,
+        )
+
+class IStudent(IStudentBase,IStudentClearance,IStudentPersonal):
+    """Representation of a student.
+
+    """
+
+class IStudentUpdateByRegNo(IStudent):
+    """Representation of a student. Skip regular reg_number validation.
+
+    """
+    reg_number = schema.TextLine(
+        title = _(u'Registration Number'),
+        default = None,
+        required = False,
+        )
+
+class IStudentUpdateByMatricNo(IStudent):
+    """Representation of a student. Skip regular matric_number validation.
+
+    """
+    matric_number = schema.TextLine(
+        title = _(u'Matriculation Number'),
+        default = None,
+        required = False,
+        )
+
+class IStudentStudyCourse(ISIRPObject):
+    """A container for student study levels.
+
+    """
+    certificate = schema.Choice(
+        title = _(u'Certificate'),
+        source = CertificateSource(),
+        default = None,
+        required = False,
+        )
+
+    entry_mode = schema.Choice(
+        title = _(u'Entry Mode'),
+        source = StudyModeSource(),
+        default = u'ug_ft',
+        required = True,
+        readonly = False,
+        )
+
+    entry_session = schema.Choice(
+        title = _(u'Entry Session'),
+        source = academic_sessions_vocab,
+        #default = datetime.now().year,
+        default = None,
+        required = True,
+        readonly = False,
+        )
+
+    current_session = schema.Choice(
+        title = _(u'Current Session'),
+        source = academic_sessions_vocab,
+        default = None,
+        required = True,
+        readonly = False,
+        )
+
+    current_level = schema.Choice(
+        title = _(u'Current Level'),
+        source = StudyLevelSource(),
+        default = None,
+        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 IStudentStudyLevel(ISIRPObject):
+    """A container for course tickets.
+
+    """
+    level = Attribute('The level code')
+    validation_date = Attribute('The date of validation')
+    validated_by = Attribute('User Id of course adviser')
+
+    level_session = schema.Choice(
+        title = _(u'Session'),
+        source = academic_sessions_vocab,
+        default = None,
+        required = True,
+        )
+
+    level_verdict = schema.Choice(
+        title = _(u'Verdict'),
+        source = VerdictSource(),
+        default = '0',
+        required = False,
+        )
+
+class ICourseTicket(ISIRPObject):
+    """A course ticket.
+
+    """
+    code = Attribute('code of the original course')
+    title = Attribute('title of the original course')
+    credits = Attribute('credits of the original course')
+    passmark = Attribute('passmark of the original course')
+    semester = Attribute('semester of the original course')
+    fcode = Attribute('faculty code of the original course')
+    dcode = Attribute('department code of the original course')
+
+    mandatory = schema.Bool(
+        title = _(u'Mandatory'),
+        default = False,
+        required = False,
+        readonly = False,
+        )
+
+    score = schema.Int(
+        title = _(u'Score'),
+        default = 0,
+        required = False,
+        readonly = False,
+        )
+
+    automatic = schema.Bool(
+        title = _(u'Automatical Creation'),
+        default = False,
+        required = False,
+        readonly = True,
+        )
+
+    carry_over = schema.Bool(
+        title = _(u'Carry-over Course'),
+        default = False,
+        required = False,
+        readonly = False,
+        )
+
+    def getLevel():
+        """Returns the id of the level the ticket has been added to.
+        """
+
+    def getLevelSession():
+        """Returns the session of the level the ticket has been added to.
+        """
+
+class ICourseTicketAdd(ICourseTicket):
+    """An interface for adding course tickets.
+
+    """
+    course = schema.Choice(
+        title = _(u'Course'),
+        source = CourseSource(),
+        readonly = False,
+        )
+
+class IStudentAccommodation(ISIRPObject):
+    """A container for student accommodation objects.
+
+    """
+
+class IBedTicket(ISIRPObject):
+    """A ticket for accommodation booking.
+
+    """
+    bed = Attribute('The bed object.')
+
+    bed_coordinates = schema.TextLine(
+        title = _(u'Bed Coordinates'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    bed_type = schema.TextLine(
+        title = _(u'Bed Type'),
+        default = None,
+        required = False,
+        readonly = False,
+        )
+
+    booking_session = schema.Choice(
+        title = _(u'Session'),
+        source = academic_sessions_vocab,
+        default = None,
+        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'),
+        default = u'',
+        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_session = schema.Choice(
+        title = _(u'Payment Session'),
+        source = academic_sessions_vocab,
+        required = False,
+        )
+
+IStudentOnlinePayment['p_session'].order = IStudentOnlinePayment[
+    'p_item'].order
+
+class IStudentChangePassword(ISIRPObject):
+    """Interface needed for change pasword page.
+
+    """
+
+    reg_number = schema.TextLine(
+        title = _(u'Registration Number'),
+        default = None,
+        required = True,
+        readonly = False,
+        )
+
+    email = schema.ASCIILine(
+        title = _(u'Email Address'),
+        required = True,
+        constraint=validate_email,
+        )
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/lgas.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/lgas.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/lgas.py	(revision 7760)
@@ -0,0 +1,880 @@
+LGAS = (
+    ('abia_aba_north','Abia / Aba North'),
+    ('abia_aba_south','Abia / Aba South'),
+    ('abia_arochukwu','Abia / Arochukwu'),
+    ('abia_bende','Abia / Bende'),
+    ('abia_ikawuno','Abia / Ikawuno'),
+    ('abia_ikwuano','Abia / Ikwuano'),
+    ('abia_isiala-ngwa-north','Abia / Isiala-Ngwa-North'),
+    ('abia_isiala-ngwa-south','Abia / Isiala-Ngwa-South'),
+    ('abia_isikwuato','Abia / Isikwuato'),
+    ('abia_isuikwuato','Abia / Isuikwuato'),
+    ('abia_nneochi','Abia / Nneochi'),
+    ('abia_obi_ngwa','Abia / Obi Ngwa'),
+    ('abia_obioma_ngwa_','Abia / Obioma Ngwa'),
+    ('abia_obioma-ngwa','Abia / Obioma-Ngwa'),
+    ('abia_ohafia','Abia / Ohafia'),
+    ('abia_ohaozara','Abia / Ohaozara'),
+    ('abia_osisioma','Abia / Osisioma'),
+    ('abia_ugwunagbo','Abia / Ugwunagbo'),
+    ('abia_ukwa_west','Abia / Ukwa West'),
+    ('abia_ukwu_east','Abia / Ukwu East'),
+    ('abia_umuahia','Abia / Umuahia'),
+    ('abia_umuahia_south','Abia / Umuahia South'),
+    ('abia_umuahia-north','Abia / Umuahia-North'),
+    ('abuja_abaji','Abuja / Abaji'),
+    ('abuja_amac','Abuja / Amac'),
+    ('abuja_bwari','Abuja / Bwari'),
+    ('abuja_kuje','Abuja / Kuje'),
+    ('abuja_gwagwalada','Abuja / Gwagwalada'),
+    ('abuja_kwali','Abuja / Kwali'),
+    ('abuja_municipal','Abuja / Municipal'),
+    ('adamawa_demsa','Adamawa / Demsa'),
+    ('adamawa_fufore','Adamawa / Fufore'),
+    ('adamawa_ganye','Adamawa / Ganye'),
+    ('adamawa_girei','Adamawa / Girei'),
+    ('adamawa_gombi','Adamawa / Gombi'),
+    ('adamawa_guyuk','Adamawa / Guyuk'),
+    ('adamawa_hong','Adamawa / Hong'),
+    ('adamawa_jada','Adamawa / Jada'),
+    ('adamawa_jimeta','Adamawa / Jimeta'),
+    ('adamawa_lamurde','Adamawa / Lamurde'),
+    ('adamawa_madagali','Adamawa / Madagali'),
+    ('adamawa_maiha','Adamawa / Maiha'),
+    ('adamawa_mayo-belwa','Adamawa / Mayo-Belwa'),
+    ('adamawa_michika','Adamawa / Michika'),
+    ('adamawa_mubi-north','Adamawa / Mubi-North'),
+    ('adamawa_mubi-south','Adamawa / Mubi-South'),
+    ('adamawa_numan','Adamawa / Numan'),
+    ('adamawa_shelleng','Adamawa / Shelleng'),
+    ('adamawa_song','Adamawa / Song'),
+    ('adamawa_toungo','Adamawa / Toungo'),
+    ('adamawa_yola','Adamawa / Yola'),
+    ('akwa_ibom_abak','Akwa Ibom / Abak'),
+    ('akwa_ibom_eastern-obolo','Akwa Ibom / Eastern-Obolo'),
+    ('akwa_ibom_eket','Akwa Ibom / Eket'),
+    ('akwa_ibom_ekpe-atani','Akwa Ibom / Ekpe-Atani'),
+    ('akwa_ibom_esit_ekit','Akwa Ibom / Esit Ekit'),
+    ('akwa_ibom_essien-udim','Akwa Ibom / Essien-Udim'),
+    ('akwa_ibom_etim-ekpo','Akwa Ibom / Etim-Ekpo'),
+    ('akwa_ibom_etinam','Akwa Ibom / Etinam'),
+    ('akwa_ibom_ibeno','Akwa Ibom / Ibeno'),
+    ('akwa_ibom_ibesikpo-asutan','Akwa Ibom / Ibesikpo-Asutan'),
+    ('akwa_ibom_ibiono-ibom','Akwa Ibom / Ibiono-Ibom'),
+    ('akwa_ibom_ika','Akwa Ibom / Ika'),
+    ('akwa_ibom_ikono','Akwa Ibom / Ikono'),
+    ('akwa_ibom_ikot-abasi','Akwa Ibom / Ikot-Abasi'),
+    ('akwa_ibom_ikot-ekpene','Akwa Ibom / Ikot-Ekpene'),
+    ('akwa_ibom_ini','Akwa Ibom / Ini'),
+    ('akwa_ibom_itu','Akwa Ibom / Itu'),
+    ('akwa_ibom_mbo','Akwa Ibom / Mbo'),
+    ('akwa_ibom_mkpat-enin','Akwa Ibom / Mkpat-Enin'),
+    ('akwa_ibom_nsit-ibom','Akwa Ibom / Nsit-Ibom'),
+    ('akwa_ibom_nsit-ubium','Akwa Ibom / Nsit-Ubium'),
+    ('akwa_ibom_obot-akara','Akwa Ibom / Obot-Akara'),
+    ('akwa_ibom_okobo','Akwa Ibom / Okobo'),
+    ('akwa_ibom_onna','Akwa Ibom / Onna'),
+    ('akwa_ibom_oron','Akwa Ibom / Oron'),
+    ('akwa_ibom_oruk_anam','Akwa Ibom / Oruk-Anam'),
+    ('akwa_ibom_udung-uko','Akwa Ibom / Udung-Uko'),
+    ('akwa_ibom_ukanefun','Akwa Ibom / Ukanefun'),
+    ('akwa_ibom_uquo_ibeno','Akwa Ibom / Uquo Ibeno'),
+    ('akwa_ibom_urue-offong-oruko','Akwa Ibom / Urue-Offong-Oruko'),
+    ('akwa_ibom_uruan','Akwa Ibom / Uruan'),
+    ('akwa_ibom_uruk-anam','Akwa Ibom / Uruk-Anam'),
+    ('akwa_ibom_uyo','Akwa Ibom / Uyo'),
+    ('anambra_aguata','Anambra / Aguata'),
+    ('anambra_anambra-east','Anambra / Anambra-East'),
+    ('anambra_anambra-west','Anambra / Anambra-West'),
+    ('anambra_anaocha','Anambra / Anaocha'),
+    ('anambra_awka-north','Anambra / Awka-North'),
+    ('anambra_awka-south','Anambra / Awka-South'),
+    ('anambra_ayamelum','Anambra / Ayamelum'),
+    ('anambra_dunukofia','Anambra / Dunukofia'),
+    ('anambra_ekwusigo','Anambra / Ekwusigo'),
+    ('anambra_idemili','Anambra / Idemili'),
+    ('anambra_idemili-north','Anambra / Idemili-North'),
+    ('anambra_idemili-south','Anambra / Idemili-South'),
+    ('anambra_ihiala','Anambra / Ihiala'),
+    ('anambra_nibo','Anambra / Nibo'),
+    ('anambra_njikoka','Anambra / Njikoka'),
+    ('anambra_nnewi','Anambra / Nnewi'),
+    ('anambra_nnewi-north','Anambra / Nnewi-North'),
+    ('anambra_nnewi-south','Anambra / Nnewi-South'),
+    ('anambra_ogbaru','Anambra / Ogbaru'),
+    ('anambra_olumba','Anambra / Olumba'),
+    ('anambra_onitsha-north','Anambra / Onitsha-North'),
+    ('anambra_onitsha-south','Anambra / Onitsha-South'),
+    ('anambra_orumba-north','Anambra / Orumba-North'),
+    ('anambra_orumba-south','Anambra / Orumba-South'),
+    ('anambra_oti','Anambra / Oti'),
+    ('anambra_oyi','Anambra / Oyi'),
+    ('anambra_ubuluizor-ihiala','Anambra / Ubuluizor-Ihiala'),
+    ('anambra_uyi','Anambra / Uyi'),
+    ('bauchi_alkaleri','Bauchi / Alkaleri'),
+    ('bauchi_bauchi','Bauchi / Bauchi'),
+    ('bauchi_bogoro','Bauchi / Bogoro'),
+    ('bauchi_damban','Bauchi / Damban'),
+    ('bauchi_darazo','Bauchi / Darazo'),
+    ('bauchi_dass','Bauchi / Dass'),
+    ('bauchi_gamawa','Bauchi / Gamawa'),
+    ('bauchi_ganjuwa','Bauchi / Ganjuwa'),
+    ('bauchi_giade','Bauchi / Giade'),
+    ('bauchi_itas/gadau','Bauchi / Itas/Gadau'),
+    ('bauchi_jamaare','Bauchi / Jamaare'),
+    ('bauchi_katagum','Bauchi / Katagum'),
+    ('bauchi_kirfi','Bauchi / Kirfi'),
+    ('bauchi_misau','Bauchi / Misau'),
+    ('bauchi_ningi','Bauchi / Ningi'),
+    ('bauchi_shira','Bauchi / Shira'),
+    ('bauchi_tafawa-balewa','Bauchi / Tafawa-Balewa'),
+    ('bauchi_toro','Bauchi / Toro'),
+    ('bauchi_warji','Bauchi / Warji'),
+    ('bauchi_zaki','Bauchi / Zaki'),
+    ('bayelsa_brass','Bayelsa / Brass'),
+    ('bayelsa_ekeremor','Bayelsa / Ekeremor'),
+    ('bayelsa_kembe','Bayelsa / Kembe'),
+    ('bayelsa_kolokuma','Bayelsa / Kolokuma'),
+    ('bayelsa_kolokuma/opkuma','Bayelsa / Kolokuma/Opkuma'),
+    ('bayelsa_nembe','Bayelsa / Nembe'),
+    ('bayelsa_ogbia','Bayelsa / Ogbia'),
+    ('bayelsa_sagbama','Bayelsa / Sagbama'),
+    ('bayelsa_southern-ijaw','Bayelsa / Southern-Ijaw'),
+    ('bayelsa_toru-abubo','Bayelsa / Toru-Abubo'),
+    ('bayelsa_yenegoa','Bayelsa / Yenegoa'),
+    ('benue_ado','Benue / Ado'),
+    ('benue_agatu','Benue / Agatu'),
+    ('benue_apa','Benue / Apa'),
+    ('benue_buruku','Benue / Buruku'),
+    ('benue_gboko','Benue / Gboko'),
+    ('benue_guma','Benue / Guma'),
+    ('benue_gwer-east','Benue / Gwer-East'),
+    ('benue_gwer-west','Benue / Gwer-West'),
+    ('benue_katsina-ala','Benue / Katsina-Ala'),
+    ('benue_konshisha','Benue / Konshisha'),
+    ('benue_kwande','Benue / Kwande'),
+    ('benue_logo','Benue / Logo'),
+    ('benue_makurdi','Benue / Makurdi'),
+    ('benue_ogbadibo','Benue / Ogbadibo'),
+    ('benue_ohimini','Benue / Ohimini'),
+    ('benue_oju','Benue / Oju'),
+    ('benue_okpokwu','Benue / Okpokwu'),
+    ('benue_otukpo','Benue / Otukpo'),
+    ('benue_oturkpa','Benue / Oturkpa'),
+    ('benue_tarka','Benue / Tarka'),
+    ('benue_ukum','Benue / Ukum'),
+    ('benue_ushongo','Benue / Ushongo'),
+    ('benue_vandeikya','Benue / Vandeikya'),
+    ('borno_abadan','Borno / Abadan'),
+    ('borno_askira-uba','Borno / Askira-Uba'),
+    ('borno_bama','Borno / Bama'),
+    ('borno_bayo','Borno / Bayo'),
+    ('borno_biu','Borno / Biu'),
+    ('borno_chibok','Borno / Chibok'),
+    ('borno_damboa','Borno / Damboa'),
+    ('borno_dikwa','Borno / Dikwa'),
+    ('borno_gubio','Borno / Gubio'),
+    ('borno_guzamala','Borno / Guzamala'),
+    ('borno_gwoza','Borno / Gwoza'),
+    ('borno_hawul','Borno / Hawul'),
+    ('borno_jere','Borno / Jere'),
+    ('borno_kaga','Borno / Kaga'),
+    ('borno_kala/balge','Borno / Kala/Balge'),
+    ('borno_konduga','Borno / Konduga'),
+    ('borno_kukawa','Borno / Kukawa'),
+    ('borno_kwaya-kusar','Borno / Kwaya-Kusar'),
+    ('borno_mafa','Borno / Mafa'),
+    ('borno_magumeri','Borno / Magumeri'),
+    ('borno_maiduguri','Borno / Maiduguri'),
+    ('borno_marte','Borno / Marte'),
+    ('borno_mobbar','Borno / Mobbar'),
+    ('borno_mongunu','Borno / Mongunu'),
+    ('borno_ngala','Borno / Ngala'),
+    ('borno_nganzai','Borno / Nganzai'),
+    ('borno_shani','Borno / Shani'),
+    ('cross_river_abi','Cross River / Abi'),
+    ('cross_river_abuochichie','Cross River / Abuochichie'),
+    ('cross_river_akamkpa','Cross River / Akamkpa'),
+    ('cross_river_akpabuyo','Cross River / Akpabuyo'),
+    ('cross_river_bakassi','Cross River / Bakassi'),
+    ('cross_river_bekwara','Cross River / Bekwara'),
+    ('cross_river_biasi','Cross River / Biasi'),
+    ('cross_river_boki','Cross River / Boki'),
+    ('cross_river_calabar-municipal','Cross River / Calabar-Municipal'),
+    ('cross_river_calabar-south','Cross River / Calabar-South'),
+    ('cross_river_etunk','Cross River / Etunk'),
+    ('cross_river_ikom','Cross River / Ikom'),
+    ('cross_river_obanliku','Cross River / Obanliku'),
+    ('cross_river_obubra','Cross River / Obubra'),
+    ('cross_river_obudu','Cross River / Obudu'),
+    ('cross_river_odukpani','Cross River / Odukpani'),
+    ('cross_river_ogoja','Cross River / Ogoja'),
+    ('cross_river_ugep-north','Cross River / Ugep-North'),
+    ('cross_river_yakurr','Cross River / Yakurr'),
+    ('cross_river_yala','Cross River / Yala'),
+    ('delta_aniocha-north','Delta / Aniocha-North'),
+    ('delta_aniocha-south','Delta / Aniocha-South'),
+    ('delta_bomadi','Delta / Bomadi'),
+    ('delta_burutu','Delta / Burutu'),
+    ('delta_effurun','Delta / Effurun'),
+    ('delta_ethiope-east','Delta / Ethiope-East'),
+    ('delta_ethiope-west','Delta / Ethiope-West'),
+    ('delta_idu','Delta / Idu'),
+    ('delta_ika-ne','Delta / Ika-Ne'),
+    ('delta_ika-north-east','Delta / Ika-North-East'),
+    ('delta_ika-south','Delta / Ika-South'),
+    ('delta_idemili-south','Delta / Idemili-South'),
+    ('delta_isoko-north','Delta / Isoko-North'),
+    ('delta_isoko-south','Delta / Isoko-South'),
+    ('delta_ndokwa-east','Delta / Ndokwa-East'),
+    ('delta_ndokwa-north','Delta / Ndokwa-North'),
+    ('delta_ndokwa-south','Delta / Ndokwa-South'),
+    ('delta_ndokwa-west','Delta / Ndokwa-West'),
+    ('delta_okpe','Delta / Okpe'),
+    ('delta_okwuani','Delta / Okwuani'),
+    ('delta_oleh','Delta / Oleh'),
+    ('delta_oshielli-north','Delta / Oshielli-North'),
+    ('delta_oshimili','Delta / Oshimili'),
+    ('delta_oshimili-north','Delta / Oshimili-North'),
+    ('delta_oshimili-south','Delta / Oshimili-South'),
+    ('delta_osimili','Delta / Osimili'),
+    ('delta_osimili-north','Delta / Osimili-North'),
+    ('delta_osimili-south','Delta / Osimili-South'),
+    ('delta_patani','Delta / Patani'),
+    ('delta_sapele','Delta / Sapele'),
+    ('delta_udokwa','Delta / Udokwa'),
+    ('delta_udu','Delta / Udu'),
+    ('delta_ughelli-north','Delta / Ughelli-North'),
+    ('delta_ughelli-south','Delta / Ughelli-South'),
+    ('delta_ukwuani','Delta / Ukwuani'),
+    ('delta_urwie','Delta / Urwie'),
+    ('delta_uvie','Delta / Uvie'),
+    ('delta_uvwei','Delta / Uvwei'),
+    ('delta_uvwie','Delta / Uvwie'),
+    ('delta_warri-central','Delta / Warri-Central'),
+    ('delta_warri-north','Delta / Warri-North'),
+    ('delta_warri-south','Delta / Warri-South'),
+    ('ebonyi_abakaliki','Ebonyi / Abakaliki'),
+    ('ebonyi_afikpo-north','Ebonyi / Afikpo-North'),
+    ('ebonyi_afikpo-south','Ebonyi / Afikpo-South'),
+    ('ebonyi_bomadim','Ebonyi / Bomadim'),
+    ('ebonyi_ebonyi','Ebonyi / Ebonyi'),
+    ('ebonyi_ezza-north','Ebonyi / Ezza-North'),
+    ('ebonyi_ezza-south','Ebonyi / Ezza-South'),
+    ('ebonyi_ikwo','Ebonyi / Ikwo'),
+    ('ebonyi_ishielu','Ebonyi / Ishielu'),
+    ('ebonyi_ivo','Ebonyi / Ivo'),
+    ('ebonyi_izzi','Ebonyi / Izzi'),
+    ('ebonyi_obaukwu','Ebonyi / Obaukwu'),
+    ('ebonyi_ohakwu','Ebonyi / Ohakwu'),
+    ('ebonyi_ohaozara','Ebonyi / Ohaozara'),
+    ('ebonyi_onicha','Ebonyi / Onicha'),
+    ('ebonyi_ukaba','Ebonyi / Ukaba'),
+    ('edo_afokpella','Edo / Afokpella'),
+    ('edo_afuze','Edo / Afuze'),
+    ('edo_agbazilo','Edo / Agbazilo'),
+    ('edo_akoko-edo','Edo / Akoko-Edo'),
+    ('edo_egor','Edo / Egor'),
+    ('edo_esan-central','Edo / Esan-Central'),
+    ('edo_esan-north-east','Edo / Esan-North-East'),
+    ('edo_esan-north-west','Edo / Esan-North-West'),
+    ('edo_esan-south-east','Edo / Esan-South-East'),
+    ('edo_esan-west','Edo / Esan-West'),
+    ('edo_etsako-central','Edo / Etsako-Central'),
+    ('edo_etsako-east','Edo / Etsako-East'),
+    ('edo_etsako-west','Edo / Etsako-West'),
+    ('edo_igueben','Edo / Igueben'),
+    ('edo_ikpoba-okha','Edo / Ikpoba-Okha'),
+    ('edo_ohunmwode','Edo / Ohunmwode'),
+    ('edo_ologbo','Edo / Ologbo'),
+    ('edo_opoji_irrua','Edo / Opoji Irrua'),
+    ('edo_oredo','Edo / Oredo'),
+    ('edo_ohionmwon','Edo / Ohionmwon'),
+    ('edo_ovia-north-east','Edo / Ovia-North-East'),
+    ('edo_ovia-south-west','Edo / Ovia-South-West'),
+    ('edo_owan-east','Edo / Owan-East'),
+    ('edo_owan-west','Edo / Owan-West'),
+    ('edo_uhunmwonde','Edo / Uhunmwonde'),
+    ('ekiti_ado-ekiti','Ekiti / Ado-Ekiti'),
+    ('ekiti_aiyekire','Ekiti / Aiyekire'),
+    ('ekiti_efon','Ekiti / Efon'),
+    ('ekiti_ekiti-east','Ekiti / Ekiti-East'),
+    ('ekiti_ekiti-south-west','Ekiti / Ekiti-South-West'),
+    ('ekiti_ekiti-west','Ekiti / Ekiti-West'),
+    ('ekiti_emure/ise/orun','Ekiti / Emure/Ise/Orun'),
+    ('ekiti_gbonyin','Ekiti / Gbonyin'),
+    ('ekiti_ido-osi','Ekiti / Ido-Osi'),
+    ('ekiti_ijero','Ekiti / Ijero'),
+    ('ekiti_ikare','Ekiti / Ikare'),
+    ('ekiti_ikere','Ekiti / Ikere'),
+    ('ekiti_ikole','Ekiti / Ikole'),
+    ('ekiti_ilejemeje','Ekiti / Ilejemeje'),
+    ('ekiti_irepodun/ifelodun','Ekiti / Irepodun/Ifelodun'),
+    ('ekiti_ise-orun','Ekiti / Ise-Orun'),
+    ('ekiti_moba','Ekiti / Moba'),
+    ('ekiti_oye','Ekiti / Oye'),
+    ('enugu_aninri','Enugu / Aninri'),
+    ('enugu_awgu','Enugu / Awgu'),
+    ('enugu_enugu-east','Enugu / Enugu-East'),
+    ('enugu_enugu-north','Enugu / Enugu-North'),
+    ('enugu_enugu-south','Enugu / Enugu-South'),
+    ('enugu_ezeagu','Enugu / Ezeagu'),
+    ('enugu_igbo-etiti','Enugu / Igbo-Etiti'),
+    ('enugu_igbo-eze-north','Enugu / Igbo-Eze-North'),
+    ('enugu_igbo-eze-south','Enugu / Igbo-Eze-South'),
+    ('enugu_isi-uzo','Enugu / Isi-Uzo'),
+    ('enugu_nkanu-east','Enugu / Nkanu-East'),
+    ('enugu_nkanu-west','Enugu / Nkanu-West'),
+    ('enugu_nsukka','Enugu / Nsukka'),
+    ('enugu_nukanu_east','Enugu / Nukanu East'),
+    ('enugu_oji-river','Enugu / Oji-River'),
+    ('enugu_udenu','Enugu / Udenu'),
+    ('enugu_udi','Enugu / Udi'),
+    ('enugu_uzo-uwani','Enugu / Uzo-Uwani'),
+    ('gombe_akko','Gombe / Akko'),
+    ('gombe_balanga','Gombe / Balanga'),
+    ('gombe_billiri','Gombe / Billiri'),
+    ('gombe_dukku','Gombe / Dukku'),
+    ('gombe_funakaye','Gombe / Funakaye'),
+    ('gombe_gombe','Gombe / Gombe'),
+    ('gombe_kaltungo','Gombe / Kaltungo'),
+    ('gombe_kwami','Gombe / Kwami'),
+    ('gombe_nafada/bajoga','Gombe / Nafada/Bajoga'),
+    ('gombe_shomgom','Gombe / Shomgom'),
+    ('gombe_yamaltu/deba','Gombe / Yamaltu/Deba'),
+    ('imo_aboh-mbaise','Imo / Aboh-Mbaise'),
+    ('imo_ahiazu-mbaise','Imo / Ahiazu-Mbaise'),
+    ('imo_dral-esat','Imo / Dral-Esat'),
+    ('imo_ehime-mbano','Imo / Ehime-Mbano'),
+    ('imo_ezinihitte','Imo / Ezinihitte'),
+    ('imo_ideato','Imo / Ideato'),
+    ('imo_ideato-north','Imo / Ideato-North'),
+    ('imo_ideato-south','Imo / Ideato-South'),
+    ('imo_ihitte/uboma','Imo / Ihitte/Uboma'),
+    ('imo_ikeduru','Imo / Ikeduru'),
+    ('imo_isiala-mbano','Imo / Isiala-Mbano'),
+    ('imo_isu','Imo / Isu'),
+    ('imo_mbaitoli','Imo / Mbaitoli'),
+    ('imo_mbano','Imo / Mbano'),
+    ('imo_ngor-okpala','Imo / Ngor-Okpala'),
+    ('imo_njaba','Imo / Njaba'),
+    ('imo_nkwerre','Imo / Nkwerre'),
+    ('imo_nwangele','Imo / Nwangele'),
+    ('imo_obowo','Imo / Obowo'),
+    ('imo_oguta','Imo / Oguta'),
+    ('imo_ohaji-egbema','Imo / Ohaji-Egbema'),
+    ('imo_okigwe','Imo / Okigwe'),
+    ('imo_onuimo','Imo / Onuimo'),
+    ('imo_orlu','Imo / Orlu'),
+    ('imo_orsu','Imo / Orsu'),
+    ('imo_oru-east','Imo / Oru-East'),
+    ('imo_oru-west','Imo / Oru-West'),
+    ('imo_owerri-municipal','Imo / Owerri-Municipal'),
+    ('imo_owerri-north','Imo / Owerri-North'),
+    ('imo_owerri-west','Imo / Owerri-West'),
+    ('imo_ugiri-ike_ikeduru','Imo / Ugiri-Ike Ikeduru'),
+    ('imo_ugiri-ikedikeduru','Imo / Ugiri-Ikedikeduru'),
+    ('imo_unbano','Imo / Unbano'),
+    ('imo_zinihitte','Imo / Zinihitte'),
+    ('jigawa_auyo','Jigawa / Auyo'),
+    ('jigawa_babura','Jigawa / Babura'),
+    ('jigawa_biriniwa','Jigawa / Biriniwa'),
+    ('jigawa_birnin-kudu','Jigawa / Birnin-Kudu'),
+    ('jigawa_bosuwa','Jigawa / Bosuwa'),
+    ('jigawa_buji','Jigawa / Buji'),
+    ('jigawa_dutse','Jigawa / Dutse'),
+    ('jigawa_gagarawa','Jigawa / Gagarawa'),
+    ('jigawa_garki','Jigawa / Garki'),
+    ('jigawa_gumel','Jigawa / Gumel'),
+    ('jigawa_guri','Jigawa / Guri'),
+    ('jigawa_gwaram','Jigawa / Gwaram'),
+    ('jigawa_gwiwa','Jigawa / Gwiwa'),
+    ('jigawa_hadejia','Jigawa / Hadejia'),
+    ('jigawa_jahun','Jigawa / Jahun'),
+    ('jigawa_kafin-hausa','Jigawa / Kafin-Hausa'),
+    ('jigawa_kaugama','Jigawa / Kaugama'),
+    ('jigawa_kazaure','Jigawa / Kazaure'),
+    ('jigawa_kirkasamma','Jigawa / Kirkasamma'),
+    ('jigawa_maigatari','Jigawa / Maigatari'),
+    ('jigawa_malam-maduri','Jigawa / Malam-Maduri'),
+    ('jigawa_miga','Jigawa / Miga'),
+    ('jigawa_ringim','Jigawa / Ringim'),
+    ('jigawa_roni','Jigawa / Roni'),
+    ('jigawa_sule-tankarkar','Jigawa / Sule-Tankarkar'),
+    ('jigawa_taura','Jigawa / Taura'),
+    ('jigawa_yankwashi','Jigawa / Yankwashi'),
+    ('kaduna_birnin-gwari','Kaduna / Birnin-Gwari'),
+    ('kaduna_chikun','Kaduna / Chikun'),
+    ('kaduna_giwa','Kaduna / Giwa'),
+    ('kaduna_gwagwada','Kaduna / Gwagwada'),
+    ('kaduna_igabi','Kaduna / Igabi'),
+    ('kaduna_ikara','Kaduna / Ikara'),
+    ('kaduna_jaba','Kaduna / Jaba'),
+    ('kaduna_jemaa','Kaduna / Jemaa'),
+    ('kaduna_kachia','Kaduna / Kachia'),
+    ('kaduna_kaduna-north','Kaduna / Kaduna-North'),
+    ('kaduna_kagarko','Kaduna / Kagarko'),
+    ('kaduna_kajuru','Kaduna / Kajuru'),
+    ('kaduna_kaura','Kaduna / Kaura'),
+    ('kaduna_kauru','Kaduna / Kauru'),
+    ('kaduna_koka/kawo','Kaduna / Koka/Kawo'),
+    ('kaduna_kubah','Kaduna / Kubah'),
+    ('kaduna_kudan','Kaduna / Kudan'),
+    ('kaduna_lere','Kaduna / Lere'),
+    ('kaduna_makarfi','Kaduna / Makarfi'),
+    ('kaduna_sabon-gari','Kaduna / Sabon-Gari'),
+    ('kaduna_sanga','Kaduna / Sanga'),
+    ('kaduna_soba','Kaduna / Soba'),
+    ('kaduna_tudun-wada/makera','Kaduna / Tudun-Wada/Makera'),
+    ('kaduna_zango-kataf','Kaduna / Zango-Kataf'),
+    ('kaduna_zaria','Kaduna / Zaria'),
+    ('kano_ajingi','Kano / Ajingi'),
+    ('kano_albasu','Kano / Albasu'),
+    ('kano_bagwai','Kano / Bagwai'),
+    ('kano_bebeji','Kano / Bebeji'),
+    ('kano_bichi','Kano / Bichi'),
+    ('kano_bunkure','Kano / Bunkure'),
+    ('kano_dala','Kano / Dala'),
+    ('kano_dambatta','Kano / Dambatta'),
+    ('kano_dawakin-kudu','Kano / Dawakin-Kudu'),
+    ('kano_dawakin-tofa','Kano / Dawakin-Tofa'),
+    ('kano_doguwa','Kano / Doguwa'),
+    ('kano_fagge','Kano / Fagge'),
+    ('kano_gabasawa','Kano / Gabasawa'),
+    ('kano_garko','Kano / Garko'),
+    ('kano_garun-mallam','Kano / Garun-Mallam'),
+    ('kano_gaya','Kano / Gaya'),
+    ('kano_gezawa','Kano / Gezawa'),
+    ('kano_gwale','Kano / Gwale'),
+    ('kano_gwarzo','Kano / Gwarzo'),
+    ('kano_kano-municipal','Kano / Kano-Municipal'),
+    ('kano_karaye','Kano / Karaye'),
+    ('kano_kibiya','Kano / Kibiya'),
+    ('kano_kiru','Kano / Kiru'),
+    ('kano_kumbotso','Kano / Kumbotso'),
+    ('kano_kunchi','Kano / Kunchi'),
+    ('kano_kura','Kano / Kura'),
+    ('kano_madobi','Kano / Madobi'),
+    ('kano_makoda','Kano / Makoda'),
+    ('kano_minjibir','Kano / Minjibir'),
+    ('kano_nasarawa','Kano / Nasarawa'),
+    ('kano_rano','Kano / Rano'),
+    ('kano_rimin-gado','Kano / Rimin-Gado'),
+    ('kano_rogo','Kano / Rogo'),
+    ('kano_shanono','Kano / Shanono'),
+    ('kano_sumaila','Kano / Sumaila'),
+    ('kano_takai','Kano / Takai'),
+    ('kano_tarauni','Kano / Tarauni'),
+    ('kano_tofa','Kano / Tofa'),
+    ('kano_tsanyawa','Kano / Tsanyawa'),
+    ('kano_tudun-wada','Kano / Tudun-Wada'),
+    ('kano_ungogo','Kano / Ungogo'),
+    ('kano_warawa','Kano / Warawa'),
+    ('kano_wudil','Kano / Wudil'),
+    ('katsina_bakori','Katsina / Bakori'),
+    ('katsina_batagarawa','Katsina / Batagarawa'),
+    ('katsina_batsari','Katsina / Batsari'),
+    ('katsina_baure','Katsina / Baure'),
+    ('katsina_bindawa','Katsina / Bindawa'),
+    ('katsina_charanchi','Katsina / Charanchi'),
+    ('katsina_dan-musa','Katsina / Dan-Musa'),
+    ('katsina_dandume','Katsina / Dandume'),
+    ('katsina_danji','Katsina / Danji'),
+    ('katsina_daura','Katsina / Daura'),
+    ('katsina_dutsi','Katsina / Dutsi'),
+    ('katsina_dutsinma','Katsina / Dutsinma'),
+    ('katsina_faskari','Katsina / Faskari'),
+    ('katsina_funtua','Katsina / Funtua'),
+    ('katsina_ingawa','Katsina / Ingawa'),
+    ('katsina_jibia','Katsina / Jibia'),
+    ('katsina_kafur','Katsina / Kafur'),
+    ('katsina_kaita','Katsina / Kaita'),
+    ('katsina_kankara','Katsina / Kankara'),
+    ('katsina_kankia','Katsina / Kankia'),
+    ('katsina_katsina','Katsina / Katsina'),
+    ('katsina_kurfi','Katsina / Kurfi'),
+    ('katsina_kusada','Katsina / Kusada'),
+    ('katsina_mai-adua','Katsina / Mai-Adua'),
+    ('katsina_malumfashi','Katsina / Malumfashi'),
+    ('katsina_mani','Katsina / Mani'),
+    ('katsina_mashi','Katsina / Mashi'),
+    ('katsina_matazu','Katsina / Matazu'),
+    ('katsina_musawa','Katsina / Musawa'),
+    ('katsina_rimi','Katsina / Rimi'),
+    ('katsina_sabuwa','Katsina / Sabuwa'),
+    ('katsina_safana','Katsina / Safana'),
+    ('katsina_sandamu','Katsina / Sandamu'),
+    ('katsina_zango','Katsina / Zango'),
+    ('kebbi_aleiro','Kebbi / Aleiro'),
+    ('kebbi_arewa-dandi','Kebbi / Arewa-Dandi'),
+    ('kebbi_argungu','Kebbi / Argungu'),
+    ('kebbi_augie','Kebbi / Augie'),
+    ('kebbi_bagudo','Kebbi / Bagudo'),
+    ('kebbi_birnin-kebbi','Kebbi / Birnin-Kebbi'),
+    ('kebbi_bumza','Kebbi / Bumza'),
+    ('kebbi_dandi','Kebbi / Dandi'),
+    ('kebbi_danko','Kebbi / Danko'),
+    ('kebbi_fakai','Kebbi / Fakai'),
+    ('kebbi_gwandu','Kebbi / Gwandu'),
+    ('kebbi_jega','Kebbi / Jega'),
+    ('kebbi_kalgo','Kebbi / Kalgo'),
+    ('kebbi_koko-besse','Kebbi / Koko-Besse'),
+    ('kebbi_maiyama','Kebbi / Maiyama'),
+    ('kebbi_ngaski','Kebbi / Ngaski'),
+    ('kebbi_sakaba','Kebbi / Sakaba'),
+    ('kebbi_shanga','Kebbi / Shanga'),
+    ('kebbi_suru','Kebbi / Suru'),
+    ('kebbi_wasagu','Kebbi / Wasagu'),
+    ('kebbi_yauri','Kebbi / Yauri'),
+    ('kebbi_zuru','Kebbi / Zuru'),
+    ('kogi_adavi','Kogi / Adavi'),
+    ('kogi_ajaokuta','Kogi / Ajaokuta'),
+    ('kogi_ankpa','Kogi / Ankpa'),
+    ('kogi_bassa','Kogi / Bassa'),
+    ('kogi_dekina','Kogi / Dekina'),
+    ('kogi_ibaji','Kogi / Ibaji'),
+    ('kogi_idah','Kogi / Idah'),
+    ('kogi_igalamela','Kogi / Igalamela'),
+    ('kogi_ijumu','Kogi / Ijumu'),
+    ('kogi_ikoyi-ijumu','Kogi / Ikoyi-Ijumu'),
+    ('kogi_kabba/bunu','Kogi / Kabba/Bunu'),
+    ('kogi_kogi','Kogi / Kogi'),
+    ('kogi_lokoja','Kogi / Lokoja'),
+    ('kogi_mopa-muro','Kogi / Mopa-Muro'),
+    ('kogi_obaji','Kogi / Obaji'),
+    ('kogi_ofu','Kogi / Ofu'),
+    ('kogi_ogori/magongo','Kogi / Ogori/Magongo'),
+    ('kogi_okehi','Kogi / Okehi'),
+    ('kogi_okene','Kogi / Okene'),
+    ('kogi_olamaboro','Kogi / Olamaboro'),
+    ('kogi_omala','Kogi / Omala'),
+    ('kogi_oru','Kogi / Oru'),
+    ('kogi_oyi','Kogi / Oyi'),
+    ('kogi_yagba-east','Kogi / Yagba-East'),
+    ('kogi_yagba-west','Kogi / Yagba-West'),
+    ('kwara_asa','Kwara / Asa'),
+    ('kwara_baruten','Kwara / Baruten'),
+    ('kwara_edu','Kwara / Edu'),
+    ('kwara_ekiti','Kwara / Ekiti'),
+    ('kwara_ilorin-east','Kwara / Ilorin-East'),
+    ('kwara_ilorin-south','Kwara / Ilorin-South'),
+    ('kwara_ilorin-west','Kwara / Ilorin-West'),
+    ('kwara_isin','Kwara / Isin'),
+    ('kwara_kaiama','Kwara / Kaiama'),
+    ('kwara_moro','Kwara / Moro'),
+    ('kwara_offa','Kwara / Offa'),
+    ('kwara_oke-ero','Kwara / Oke-Ero'),
+    ('kwara_oyun','Kwara / Oyun'),
+    ('kwara_pategi','Kwara / Pategi'),
+    ('kwara_ifelodun','Kwara / Ifelodun'),
+    ('kwara_irepodun','Kwara / Irepodun'),
+    ('lagos_agege','Lagos / Agege'),
+    ('lagos_ajeromi-ifelodun','Lagos / Ajeromi-Ifelodun'),
+    ('lagos_alimosho','Lagos / Alimosho'),
+    ('lagos_amuwo-odofin','Lagos / Amuwo-Odofin'),
+    ('lagos_apapa','Lagos / Apapa'),
+    ('lagos_badagry','Lagos / Badagry'),
+    ('lagos_epe','Lagos / Epe'),
+    ('lagos_eti-osa','Lagos / Eti-Osa'),
+    ('lagos_ibeju-lekki','Lagos / Ibeju-Lekki'),
+    ('lagos_ifako-ijaiye','Lagos / Ifako-Ijaiye'),
+    ('lagos_ikeja','Lagos / Ikeja'),
+    ('lagos_ikorodu','Lagos / Ikorodu'),
+    ('lagos_island','Lagos / Island'),
+    ('lagos_kosofe','Lagos / Kosofe'),
+    ('lagos_lagos-island','Lagos / Lagos-Island'),
+    ('lagos_lagos-mainland','Lagos / Lagos-Mainland'),
+    ('lagos_mushin','Lagos / Mushin'),
+    ('lagos_ojo','Lagos / Ojo'),
+    ('lagos_oshodi-isolo','Lagos / Oshodi-Isolo'),
+    ('lagos_shomolu','Lagos / Shomolu'),
+    ('lagos_somolu','Lagos / Somolu'),
+    ('lagos_suru-lere','Lagos / Suru-Lere'),
+    ('nassarawa_akwanga','Nassarawa / Akwanga'),
+    ('nassarawa_awe','Nassarawa / Awe'),
+    ('nassarawa_doma','Nassarawa / Doma'),
+    ('nassarawa_karu','Nassarawa / Karu'),
+    ('nassarawa_keana','Nassarawa / Keana'),
+    ('nassarawa_keffi','Nassarawa / Keffi'),
+    ('nassarawa_kokona','Nassarawa / Kokona'),
+    ('nassarawa_lafia','Nassarawa / Lafia'),
+    ('nassarawa_nassawara','Nassarawa / Nassawara'),
+    ('nassarawa_nassarawa','Nassarawa / Nassarawa'),
+    ('nassarawa_nassawara_eggon','Nassarawa / Nassawara Eggon'),
+    ('nassarawa_obi','Nassarawa / Obi'),
+    ('nassarawa_toto','Nassarawa / Toto'),
+    ('nassarawa_wambu','Nassarawa / Wambu'),
+    ('niger_agaie','Niger / Agaie'),
+    ('niger_agwara','Niger / Agwara'),
+    ('niger_bida','Niger / Bida'),
+    ('niger_borgu','Niger / Borgu'),
+    ('niger_bosso','Niger / Bosso'),
+    ('niger_chanchaga','Niger / Chanchaga'),
+    ('niger_edati','Niger / Edati'),
+    ('niger_gbako','Niger / Gbako'),
+    ('niger_gurara','Niger / Gurara'),
+    ('niger_katcha','Niger / Katcha'),
+    ('niger_kontagora','Niger / Kontagora'),
+    ('niger_lapai','Niger / Lapai'),
+    ('niger_lavun','Niger / Lavun'),
+    ('niger_magama','Niger / Magama'),
+    ('niger_mariga','Niger / Mariga'),
+    ('niger_mashegu','Niger / Mashegu'),
+    ('niger_mokwa','Niger / Mokwa'),
+    ('niger_muya','Niger / Muya'),
+    ('niger_paikoro','Niger / Paikoro'),
+    ('niger_rafi','Niger / Rafi'),
+    ('niger_rijau','Niger / Rijau'),
+    ('niger_shiroro','Niger / Shiroro'),
+    ('niger_suleja','Niger / Suleja'),
+    ('niger_tafa','Niger / Tafa'),
+    ('niger_wushishi','Niger / Wushishi'),
+    ('ogun_abeokuta-north','Ogun / Abeokuta-North'),
+    ('ogun_abeokuta-south','Ogun / Abeokuta-South'),
+    ('ogun_ado-odo/ota','Ogun / Ado-Odo/Ota'),
+    ('ogun_adoodo-ota','Ogun / Adoodo-Ota'),
+    ('ogun_ewekoro','Ogun / Ewekoro'),
+    ('ogun_idarapo','Ogun / Idarapo'),
+    ('ogun_iffo-otta','Ogun / Iffo-Otta'),
+    ('ogun_ifo','Ogun / Ifo'),
+    ('ogun_igbado-north','Ogun / Igbado-North'),
+    ('ogun_igbado-south','Ogun / Igbado-South'),
+    ('ogun_ijebu-east','Ogun / Ijebu-East'),
+    ('ogun_ijebu-north','Ogun / Ijebu-North'),
+    ('ogun_ijebu-north-east','Ogun / Ijebu-North-East'),
+    ('ogun_ijebu-ode','Ogun / Ijebu-Ode'),
+    ('ogun_ikenne','Ogun / Ikenne'),
+    ('ogun_ilishan_remo','Ogun / Ilishan Remo'),
+    ('ogun_ilugun-alaro','Ogun / Ilugun-Alaro'),
+    ('ogun_imeko-afon','Ogun / Imeko-Afon'),
+    ('ogun_ipokia','Ogun / Ipokia'),
+    ('ogun_lgayekpe_','Ogun / Lgayekpe'),
+    ('ogun_obafemi-owode','Ogun / Obafemi-Owode'),
+    ('ogun_odedah','Ogun / Odedah'),
+    ('ogun_odogbolu','Ogun / Odogbolu'),
+    ('ogun_ogun-waterside','Ogun / Ogun-Waterside'),
+    ('ogun_remo-north','Ogun / Remo-North'),
+    ('ogun_sagamu','Ogun / Sagamu'),
+    ('ogun_shagamu','Ogun / Shagamu'),
+    ('ogun_yewa','Ogun / Yewa'),
+    ('ondo_akoko','Ondo / Akoko'),
+    ('ondo_akoko-north','Ondo / Akoko-North'),
+    ('ondo_akoko-north-east','Ondo / Akoko-North-East'),
+    ('ondo_akoko-north-west','Ondo / Akoko-North-West'),
+    ('ondo_akoko-south','Ondo / Akoko-South'),
+    ('ondo_akoko-south-east','Ondo / Akoko-South-East'),
+    ('ondo_akoko-south-west','Ondo / Akoko-South-West'),
+    ('ondo_akure','Ondo / Akure'),
+    ('ondo_akure-north','Ondo / Akure-North'),
+    ('ondo_akure-south','Ondo / Akure-South'),
+    ('ondo_ese-odo','Ondo / Ese-Odo'),
+    ('ondo_idanre','Ondo / Idanre'),
+    ('ondo_ifedore','Ondo / Ifedore'),
+    ('ondo_ikale','Ondo / Ikale'),
+    ('ondo_ilaje','Ondo / Ilaje'),
+    ('ondo_ilaje-west','Ondo / Ilaje-West'),
+    ('ondo_ile-oluji-okeigbo','Ondo / Ile-Oluji-Okeigbo'),
+    ('ondo_irele','Ondo / Irele'),
+    ('ondo_odigbo','Ondo / Odigbo'),
+    ('ondo_okiti-pupa','Ondo / Okiti-Pupa'),
+    ('ondo_ondo','Ondo / Ondo'),
+    ('ondo_ondo-east','Ondo / Ondo-East'),
+    ('ondo_ondo-west','Ondo / Ondo-West'),
+    ('ondo_ose','Ondo / Ose'),
+    ('ondo_owo','Ondo / Owo'),
+    ('osun_atakumosa','Osun / Atakumosa'),
+    ('osun_atakumosa_east','Osun / Atakumosa East'),
+    ('osun_ayeda-ade','Osun / Ayeda-Ade'),
+    ('osun_ayedire','Osun / Ayedire'),
+    ('osun_boluwaduro','Osun / Boluwaduro'),
+    ('osun_boripe','Osun / Boripe'),
+    ('osun_ede','Osun / Ede'),
+    ('osun_ede-north','Osun / Ede North'),
+    ('osun_egbedore','Osun / Egbedore'),
+    ('osun_ejigbo','Osun / Ejigbo'),
+    ('osun_ife','Osun / Ife'),
+    ('osun_ife-north','Osun / Ife-North'),
+    ('osun_ife-south','Osun / Ife-South'),
+    ('osun_ife-central','Osun / Ife-Central'),
+    ('osun_ife-east','Osun / Ife-East'),
+    ('osun_ifelodun','Osun / Ifelodun'),
+    ('osun_ila','Osun / Ila'),
+    ('osun_ilesa-east','Osun / Ilesa-East'),
+    ('osun_ilesa-west','Osun / Ilesa-West'),
+    ('osun_ilesha','Osun / Ilesha'),
+    ('osun_ilesha_west','Osun / Ilesha West'),
+    ('osun_irepodun','Osun / Irepodun'),
+    ('osun_irewole','Osun / Irewole'),
+    ('osun_isokan','Osun / Isokan'),
+    ('osun_iwo','Osun / Iwo'),
+    ('osun_obokun','Osun / Obokun'),
+    ('osun_odo-otin','Osun / Odo-Otin'),
+    ('osun_ola_oluwa','Osun / Ola Oluwa'),
+    ('osun_olorunda','Osun / Olorunda'),
+    ('osun_ori-ade','Osun / Ori-Ade'),
+    ('osun_orolu','Osun / Orolu'),
+    ('osun_osogbo','Osun / Osogbo'),
+    ('oyo_afijio','Oyo / Afijio'),
+    ('oyo_akinyele','Oyo / Akinyele'),
+    ('oyo_atiba','Oyo / Atiba'),
+    ('oyo_atigbo','Oyo / Atigbo'),
+    ('oyo_egbeda','Oyo / Egbeda'),
+    ('oyo_ibadan-central','Oyo / Ibadan-Central'),
+    ('oyo_ibadan-north-east','Oyo / Ibadan-North-East'),
+    ('oyo_ibadan-north-west','Oyo / Ibadan-North-West'),
+    ('oyo_ibadan-south-east','Oyo / Ibadan-South-East'),
+    ('oyo_ibadan-south-west','Oyo / Ibadan-South-West'),
+    ('oyo_ibarapa-central','Oyo / Ibarapa-Central'),
+    ('oyo_ibarapa-east','Oyo / Ibarapa-East'),
+    ('oyo_ibarapa-north','Oyo / Ibarapa-North'),
+    ('oyo_ido','Oyo / Ido'),
+    ('oyo_ifedayo','Oyo / Ifedayo'),
+    ('oyo_ifeloju','Oyo / Ifeloju'),
+    ('oyo_irepo','Oyo / Irepo'),
+    ('oyo_iseyin','Oyo / Iseyin'),
+    ('oyo_itseiwaju','Oyo / Itseiwaju'),
+    ('oyo_iwajowa','Oyo / Iwajowa'),
+    ('oyo_kajola','Oyo / Kajola'),
+    ('oyo_lagelu','Oyo / Lagelu'),
+    ('oyo_odo-oluwa','Oyo / Odo-Oluwa'),
+    ('oyo_ogbomosho-north','Oyo / Ogbomosho-North'),
+    ('oyo_ogbomosho-south','Oyo / Ogbomosho-South'),
+    ('oyo_olorunsogo','Oyo / Olorunsogo'),
+    ('oyo_oluyole','Oyo / Oluyole'),
+    ('oyo_ona-ara','Oyo / Ona-Ara'),
+    ('oyo_orelope','Oyo / Orelope'),
+    ('oyo_ori-ire','Oyo / Ori-Ire'),
+    ('oyo_oyo-west','Oyo / Oyo-West'),
+    ('oyo_saki-east','Oyo / Saki-East'),
+    ('oyo_saki-west','Oyo / Saki-West'),
+    ('oyo_surulere','Oyo / Surulere'),
+    ('plateau_barkin-ladi','Plateau / Barkin-Ladi'),
+    ('plateau_bassa','Plateau / Bassa'),
+    ('plateau_bokkos','Plateau / Bokkos'),
+    ('plateau_jos-east','Plateau / Jos-East'),
+    ('plateau_jos-north','Plateau / Jos-North'),
+    ('plateau_jos-south','Plateau / Jos-South'),
+    ('plateau_kanam','Plateau / Kanam'),
+    ('plateau_kanke','Plateau / Kanke'),
+    ('plateau_langtang-north','Plateau / Langtang-North'),
+    ('plateau_langtang-south','Plateau / Langtang-South'),
+    ('plateau_mangu','Plateau / Mangu'),
+    ('plateau_mikang','Plateau / Mikang'),
+    ('plateau_pankshin','Plateau / Pankshin'),
+    ('plateau_quananpan','Plateau / Quananpan'),
+    ('plateau_riyom','Plateau / Riyom'),
+    ('plateau_shendam','Plateau / Shendam'),
+    ('plateau_wase','Plateau / Wase'),
+    ('rivers_aboa/odual','Rivers / Aboa/Odual'),
+    ('rivers_ahoada-east','Rivers / Ahoada-East'),
+    ('rivers_ahoada-west','Rivers / Ahoada-West'),
+    ('rivers_akukutoru','Rivers / Akukutoru'),
+    ('rivers_andoni','Rivers / Andoni'),
+    ('rivers_asari-toru','Rivers / Asari-Toru'),
+    ('rivers_bonny','Rivers / Bonny'),
+    ('rivers_buguma','Rivers / Buguma'),
+    ('rivers_degema','Rivers / Degema'),
+    ('rivers_eleme','Rivers / Eleme'),
+    ('rivers_elfane','Rivers / Elfane'),
+    ('rivers_emuoha','Rivers / Emuoha'),
+    ('rivers_etche','Rivers / Etche'),
+    ('rivers_gokana','Rivers / Gokana'),
+    ('rivers_ikwerre','Rivers / Ikwerre'),
+    ('rivers_khana','Rivers / Khana'),
+    ('rivers_obia/akpor','Rivers / Obia/Akpor'),
+    ('rivers_ogba-egbema-ndoni','Rivers / Ogba-Egbema-Ndoni'),
+    ('rivers_ogba/egbema/ndoni','Rivers / Ogba/Egbema/Ndoni'),
+    ('rivers_ogu/bolo','Rivers / Ogu/Bolo'),
+    ('rivers_okirika','Rivers / Okirika'),
+    ('rivers_omuma','Rivers / Omuma'),
+    ('rivers_opobo/nkoro','Rivers / Opobo/Nkoro'),
+    ('rivers_oyigbo','Rivers / Oyigbo'),
+    ('rivers_port-harcourt','Rivers / Port-Harcourt'),
+    ('rivers_tai','Rivers / Tai'),
+    ('sokoto_binji','Sokoto / Binji'),
+    ('sokoto_bodinga','Sokoto / Bodinga'),
+    ('sokoto_dange-shuni','Sokoto / Dange-Shuni'),
+    ('sokoto_gada','Sokoto / Gada'),
+    ('sokoto_goronyo','Sokoto / Goronyo'),
+    ('sokoto_gudu','Sokoto / Gudu'),
+    ('sokoto_gwadabawa','Sokoto / Gwadabawa'),
+    ('sokoto_illela','Sokoto / Illela'),
+    ('sokoto_kebbe','Sokoto / Kebbe'),
+    ('sokoto_kware','Sokoto / Kware'),
+    ('sokoto_raba','Sokoto / Raba'),
+    ('sokoto_sabo-birni','Sokoto / Sabo-Birni'),
+    ('sokoto_shagari','Sokoto / Shagari'),
+    ('sokoto_silame','Sokoto / Silame'),
+    ('sokoto_sokoto-north','Sokoto / Sokoto-North'),
+    ('sokoto_sokoto-south','Sokoto / Sokoto-South'),
+    ('sokoto_tambuwal','Sokoto / Tambuwal'),
+    ('sokoto_tangaza','Sokoto / Tangaza'),
+    ('sokoto_tureta','Sokoto / Tureta'),
+    ('sokoto_wamakko','Sokoto / Wamakko'),
+    ('sokoto_wurno','Sokoto / Wurno'),
+    ('sokoto_yabo','Sokoto / Yabo'),
+    ('taraba_ardo-kola','Taraba / Ardo-Kola'),
+    ('taraba_bali','Taraba / Bali'),
+    ('taraba_donga','Taraba / Donga'),
+    ('taraba_gashaka','Taraba / Gashaka'),
+    ('taraba_gassol','Taraba / Gassol'),
+    ('taraba_ibi','Taraba / Ibi'),
+    ('taraba_jalingo','Taraba / Jalingo'),
+    ('taraba_karim-lamido','Taraba / Karim-Lamido'),
+    ('taraba_kurmi','Taraba / Kurmi'),
+    ('taraba_lau','Taraba / Lau'),
+    ('taraba_oilingo','Taraba / Oilingo'),
+    ('taraba_sardauna','Taraba / Sardauna'),
+    ('taraba_takum','Taraba / Takum'),
+    ('taraba_ussa','Taraba / Ussa'),
+    ('taraba_wukari','Taraba / Wukari'),
+    ('taraba_yorro','Taraba / Yorro'),
+    ('taraba_zing','Taraba / Zing'),
+    ('yobe_bade','Yobe / Bade'),
+    ('yobe_borsali','Yobe / Borsali'),
+    ('yobe_damaturu','Yobe / Damaturu'),
+    ('yobe_fika','Yobe / Fika'),
+    ('yobe_fune','Yobe / Fune'),
+    ('yobe_geidam','Yobe / Geidam'),
+    ('yobe_gogaram','Yobe / Gogaram'),
+    ('yobe_gujba','Yobe / Gujba'),
+    ('yobe_gulani','Yobe / Gulani'),
+    ('yobe_jakusko','Yobe / Jakusko'),
+    ('yobe_karasuwa','Yobe / Karasuwa'),
+    ('yobe_machina','Yobe / Machina'),
+    ('yobe_nangere','Yobe / Nangere'),
+    ('yobe_nguru','Yobe / Nguru'),
+    ('yobe_potiskum','Yobe / Potiskum'),
+    ('yobe_tarmua','Yobe / Tarmua'),
+    ('yobe_yunusari','Yobe / Yunusari'),
+    ('yobe_yusufari','Yobe / Yusufari'),
+    ('zamfara_anka','Zamfara / Anka'),
+    ('zamfara_bakura','Zamfara / Bakura'),
+    ('zamfara_bukkuyum','Zamfara / Bukkuyum'),
+    ('zamfara_bungudu','Zamfara / Bungudu'),
+    ('zamfara_gumi','Zamfara / Gumi'),
+    ('zamfara_gusau','Zamfara / Gusau'),
+    ('zamfara_isa','Zamfara / Isa'),
+    ('zamfara_kaura-namoda','Zamfara / Kaura-Namoda'),
+    ('zamfara_kiyawa','Zamfara / Kiyawa'),
+    ('zamfara_maradun','Zamfara / Maradun'),
+    ('zamfara_maru','Zamfara / Maru'),
+    ('zamfara_shinkafi','Zamfara / Shinkafi'),
+    ('zamfara_talata-mafara','Zamfara / Talata-Mafara'),
+    ('zamfara_tsafe','Zamfara / Tsafe'),
+    ('zamfara_zurmi','Zamfara / Zurmi'),
+    ('osun_aiyedire','Osun / Aiyedire'),
+    ('cross_river_biase','Cross River / Biase'),
+    ('ogun_egbado-north','Ogun / Egbado-North'),
+    ('ogun_egbado-south','Ogun / Egbado-South'),
+    ('oyo_itesiwaju','Oyo / Itesiwaju'),
+    ('oyo_ogo-oluwa','Oyo / Ogo-Oluwa'),
+    ('ebonyi_ohaukwu','Ebonyi / Ohaukwu'),
+    ('ondo_okitipupa','Ondo / Okitipupa'),
+    ('osun_oriade','Osun / Oriade'),
+    ('benue_oturkpo','Benue / Oturkpo'),
+    ('oyo_oyo-east','Oyo / Oyo-East'),
+    ('abia_ukwa-east','Abia / Ukwa-East'),
+    ('akwa_ibom_urue-ffong-oruko','Akwa Ibom / Urue-Offong-Oruko'),
+    ('foreigner','Foreigner')
+    )
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/nats.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/nats.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/nats.py	(revision 7760)
@@ -0,0 +1,253 @@
+NATS = (
+    ('nigeria','Nigeria'),
+    ('afghanistan','Afghanistan'),
+    ('albania','Albania'),
+    ('algeria','Algeria'),
+    ('american_samoa','American Samoa'),
+    ('andorra','Andorra'),
+    ('angola','Angola'),
+    ('anguilla','Anguilla'),
+    ('antarctica','Antarctica'),
+    ('antigua_and_barbuda','Antigua And Barbuda'),
+    ('argentina','Argentina'),
+    ('armenia','Armenia'),
+    ('aruba','Aruba'),
+    ('ascension_island','Ascension Island'),
+    ('australia','Australia'),
+    ('austria','Austria'),
+    ('azerbaijan','Azerbaijan'),
+    ('bahamas','Bahamas'),
+    ('bahrain','Bahrain'),
+    ('bangladesh','Bangladesh'),
+    ('barbados','Barbados'),
+    ('belarus','Belarus'),
+    ('belgium','Belgium'),
+    ('belize','Belize'),
+    ('benin','Benin'),
+    ('bermuda','Bermuda'),
+    ('bhutan','Bhutan'),
+    ('bolivia','Bolivia'),
+    ('bosnia_and_herzegovina','Bosnia And Herzegovina'),
+    ('botswana','Botswana'),
+    ('bouvet_island','Bouvet Island'),
+    ('brazil','Brazil'),
+    ('british_indian_ocean_territory','British Indian Ocean Territory'),
+    ('british_virgin_islands','British Virgin Islands'),
+    ('brunei_darussalam','Brunei Darussalam'),
+    ('bulgaria','Bulgaria'),
+    ('burkina_faso','Burkina Faso'),
+    ('burma','Burma'),
+    ('burundi','Burundi'),
+    ('cambodia','Cambodia'),
+    ('cameroon','Cameroon'),
+    ('canada','Canada'),
+    ('cape_verde','Cape Verde'),
+    ('cayman_islands','Cayman Islands'),
+    ('central_african_republic','Central African Republic'),
+    ('chad','Chad'),
+    ('chile','Chile'),
+    ('china','China'),
+    ('christmas_island','Christmas Island'),
+    ('cocos_keeling_islands','Cocos (Keeling) Islands'),
+    ('colombia','Colombia'),
+    ('comoros','Comoros'),
+    ('congo_brazzaville_','Congo (Brazzaville) '),
+    ('congo_dem_rep','Congo, Democratic Republic Of'),
+    ('cook_islands','Cook Islands'),
+    ('costa_rica','Costa Rica'),
+    ('cote_d_ivoire',"Cote D'Ivoire"),
+    ('croatia','Croatia'),
+    ('cuba','Cuba'),
+    ('cyprus','Cyprus'),
+    ('czech_republic','Czech Republic'),
+    ('denmark','Denmark'),
+    ('djibouti','Djibouti'),
+    ('dominica','Dominica'),
+    ('dominican_republic','Dominican Republic'),
+    ('east_timor','East Timor'),
+    ('ecuador','Ecuador'),
+    ('egypt','Egypt'),
+    ('el_salvador','El Salvador'),
+    ('england','England'),
+    ('equatorial_guinea','Equatorial Guinea'),
+    ('eritrea','Eritrea'),
+    ('estonia','Estonia'),
+    ('ethiopia','Ethiopia'),
+    ('falkland_islands','Falkland Islands (Islas Malvinas) '),
+    ('faroe_islands','Faroe Islands'),
+    ('fiji','Fiji'),
+    ('finland','Finland'),
+    ('france','France'),
+    ('french_guiana','French Guiana'),
+    ('french_polynesia','French Polynesia'),
+    ('french_southern_territories','French Southern Territories'),
+    ('gabon','Gabon'),
+    ('gambia','Gambia, The'),
+    ('georgia','Georgia'),
+    ('germany','Germany'),
+    ('ghana','Ghana'),
+    ('gibraltar','Gibraltar'),
+    ('greece','Greece'),
+    ('greenland_denmark_','Greenland (Denmark) '),
+    ('grenada','Grenada'),
+    ('guadeloupe','Guadeloupe'),
+    ('guam','Guam'),
+    ('guatemala','Guatemala'),
+    ('guinea','Guinea'),
+    ('guinea-bissau','Guinea-Bissau'),
+    ('guyana','Guyana'),
+    ('haiti','Haiti'),
+    ('heard_and_mcdonald_islands','Heard And McDonald Islands'),
+    ('honduras','Honduras'),
+    ('hong_kong','Hong Kong (Special Admin Region Of China) '),
+    ('hungary','Hungary'),
+    ('iceland','Iceland'),
+    ('india','India'),
+    ('indonesia','Indonesia'),
+    ('iran','Iran'),
+    ('iraq','Iraq'),
+    ('ireland','Ireland'),
+    ('isle_of_man','Isle Of Man'),
+    ('israel','Israel'),
+    ('italy','Italy'),
+    ('jamaica','Jamaica'),
+    ('japan','Japan'),
+    ('johnston_atoll','Johnston Atoll'),
+    ('jordan','Jordan'),
+    ('kazakhstan','Kazakhstan'),
+    ('kenya','Kenya'),
+    ('kiribati','Kiribati'),
+    ('korea_north_','Korea, Democratic People S Republic Of (North) '),
+    ('korea_south_','Korea, Republic Of (South) '),
+    ('kuwait','Kuwait'),
+    ('kyrgyzstan','Kyrgyzstan'),
+    ('laos_','Lao People S Democratic Republic (Laos) '),
+    ('latvia','Latvia'),
+    ('lebanon','Lebanon'),
+    ('lesotho','Lesotho'),
+    ('liberia','Liberia'),
+    ('libya','Libyan Arab Jamahiriya'),
+    ('liechtenstein','Liechtenstein'),
+    ('lithuania','Lithuania'),
+    ('luxembourg','Luxembourg'),
+    ('macau','Macau (Special Admin Region Of China) '),
+    ('macedonia','Macedonia, Former Yugoslav Republic Of'),
+    ('madagascar','Madagascar'),
+    ('malawi','Malawi'),
+    ('malaysia','Malaysia'),
+    ('maldives','Maldives'),
+    ('mali','Mali'),
+    ('malta','Malta'),
+    ('marshall_islands','Marshall Islands'),
+    ('martinique','Martinique'),
+    ('mauritania','Mauritania'),
+    ('mauritius','Mauritius'),
+    ('mayotte','Mayotte'),
+    ('mexico','Mexico'),
+    ('micronesia','Micronesia, Federated States Of'),
+    ('midway_islands','Midway Islands'),
+    ('moldova','Moldova, Republic Of'),
+    ('monaco','Monaco'),
+    ('mongolia','Mongolia'),
+    ('montserrat','Montserrat'),
+    ('morocco','Morocco'),
+    ('mozambique','Mozambique'),
+    ('myanmar','Myanmar'),
+    ('namibia','Namibia'),
+    ('nauru','Nauru'),
+    ('nepal','Nepal'),
+    ('netherlands','Netherlands'),
+    ('netherlands_antilles','Netherlands Antilles'),
+    ('new_caledonia','New Caledonia'),
+    ('new_zealand','New Zealand'),
+    ('nicaragua','Nicaragua'),
+    ('niger','Niger'),
+    ('niue','Niue'),
+    ('norfolk_island','Norfolk Island'),
+    ('northern_mariana_islands','Northern Mariana Islands'),
+    ('norway','Norway'),
+    ('oman','Oman'),
+    ('pakistan','Pakistan'),
+    ('palau','Palau'),
+    ('palestinian_territory','Palestinian Territory, Occupied'),
+    ('panama','Panama'),
+    ('papua_new_guinea','Papua New Guinea'),
+    ('paraguay','Paraguay'),
+    ('peru','Peru'),
+    ('philippines','Philippines'),
+    ('pitcairn_islands','Pitcairn Islands'),
+    ('poland','Poland'),
+    ('portugal','Portugal'),
+    ('puerto_rico','Puerto Rico'),
+    ('qatar','Qatar'),
+    ('reunion','Reunion'),
+    ('romania','Romania'),
+    ('russian_federation','Russian Federation'),
+    ('rwanda','Rwanda'),
+    ('saint_helena_island','Saint Helena Island'),
+    ('saint_kitts_and_nevis','Saint Kitts And Nevis'),
+    ('saint_lucia','Saint Lucia'),
+    ('saint_pierre_and_miquelon','Saint Pierre And Miquelon'),
+    ('saint_vincent_and_the_grenadines','Saint Vincent And The Grenadines'),
+    ('samoa','Samoa'),
+    ('san_marino','San Marino'),
+    ('sao_tome_and_principe','Sao Tome And Principe'),
+    ('saudi_arabia','Saudi Arabia'),
+    ('scotland','Scotland'),
+    ('senegal','Senegal'),
+    ('seychelles','Seychelles'),
+    ('sierra_leone','Sierra Leone'),
+    ('singapore','Singapore'),
+    ('slovakia','Slovakia'),
+    ('slovenia','Slovenia'),
+    ('solomon_islands','Solomon Islands'),
+    ('somalia','Somalia'),
+    ('south_africa','South Africa'),
+    ('south_georgia_south_sandwich_islands','South Georgia And South Sandwich Islands'),
+    ('spain','Spain'),
+    ('sri_lanka','Sri Lanka'),
+    ('sudan','Sudan'),
+    ('suriname','Suriname'),
+    ('svalbard_and_jan_mayen','Svalbard And Jan Mayen'),
+    ('swaziland','Swaziland'),
+    ('sweden','Sweden'),
+    ('switzerland','Switzerland'),
+    ('syrian_arab_republic','Syrian Arab Republic'),
+    ('taiwan','Taiwan'),
+    ('tajikistan','Tajikistan'),
+    ('tanzania','Tanzania, United Republic Of'),
+    ('thailand','Thailand'),
+    ('tibet','Tibet'),
+    ('togo','Togo'),
+    ('tokelau','Tokelau'),
+    ('tonga','Tonga'),
+    ('trinidad_and_tobago','Trinidad And Tobago'),
+    ('tunisia','Tunisia'),
+    ('turkey','Turkey'),
+    ('turkmenistan','Turkmenistan'),
+    ('turks_and_caicos','Turks And Caicos'),
+    ('tuvalu','Tuvalu'),
+    ('uganda','Uganda'),
+    ('ukraine','Ukraine'),
+    ('united_arab_emirates','United Arab Emirates'),
+    ('united_kingdom','United Kingdom'),
+    ('united_states','United States'),
+    ('united_states_minor_outlying_islands','United States Minor Outlying Islands'),
+    ('united_states_virgin_islands','United States Virgin Islands'),
+    ('uruguay','Uruguay'),
+    ('uzbekistan','Uzbekistan'),
+    ('vanuatu','Vanuatu'),
+    ('vatican_city','Vatican City'),
+    ('venezuela','Venezuela'),
+    ('vietnam','Vietnam'),
+    ('wake_island','Wake Island'),
+    ('wales','Wales'),
+    ('wallis_and_futuna','Wallis And Futuna'),
+    ('western_sahara','Western Sahara'),
+    ('western_samoa','Western Samoa'),
+    ('yemen','Yemen'),
+    ('serbia','Serbia'),
+    ('montenegro','Montenegro'),
+    ('zambia','Zambia'),
+    ('zimbabwe','Zimbabwe'),
+    )
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/payments.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/payments.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/payments.py	(revision 7760)
@@ -0,0 +1,72 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.students.interfaces import (
+    IStudentPaymentsContainer, IStudentNavigation, IStudentOnlinePayment)
+from waeup.sirp.payments import PaymentsContainer, OnlinePayment
+from waeup.sirp.utils.helpers import attrs_to_fields
+
+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
+
+    def getStudent(self):
+        return self.__parent__
+
+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
+
+    def getStudent(self):
+        return self.__parent__.__parent__
+
+StudentOnlinePayment = attrs_to_fields(StudentOnlinePayment)
+
+# 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/permissions.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/permissions.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/permissions.py	(revision 7760)
@@ -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
+##
+"""
+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')
+
+# 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.Authenticated')
+
+# Site Roles
+class StudentRole(grok.Role):
+    grok.name('waeup.Student')
+    grok.permissions('waeup.viewAcademics', 'waeup.viewMyStudentDataTab')
+
+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.manageStudent',
+                     'waeup.payStudent', 'waeup.uploadStudentFile',
+                     'waeup.handleAccommodation', 'waeup.viewStudents')
+
+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')
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/student.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/student.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/student.py	(revision 7760)
@@ -0,0 +1,287 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 grok
+from zope.component import getUtility
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from waeup.sirp.interfaces import (
+    IObjectHistory, IUserAccount, IFileStoreNameChooser, IFileStoreHandler,
+    ISIRPUtils, CLEARANCE, registration_states_vocab)
+from waeup.sirp.image import SIRPImageFile
+from waeup.sirp.imagestorage import DefaultFileStoreHandler
+from waeup.sirp.students.interfaces import IStudent, IStudentNavigation
+from waeup.sirp.students.studycourse import StudentStudyCourse
+from waeup.sirp.students.payments import StudentPaymentsContainer
+from waeup.sirp.students.accommodation import StudentAccommodation
+from waeup.sirp.utils.helpers import attrs_to_fields
+from waeup.sirp.students.utils import generate_student_id
+
+class Student(grok.Container):
+    """This is a student container for the various objects
+    owned by students.
+    """
+    grok.implements(IStudent, IStudentNavigation)
+    grok.provides(IStudent)
+
+    def __init__(self):
+        super(Student, self).__init__()
+        # The site doesn't exist in unit tests
+        try:
+            students = grok.getSite()['students']
+            self.student_id = generate_student_id(students,'?')
+        except TypeError:
+            self.student_id = u'Z654321'
+        self.password = None
+        return
+
+    def loggerInfo(self, ob_class, comment=None):
+        target = self.__name__
+        return grok.getSite()['students'].logger_info(ob_class,target,comment)
+
+    @property
+    def display_fullname(self):
+        middlename = getattr(self, 'middlename', None)
+        sirp_utils = getUtility(ISIRPUtils)
+        return sirp_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
+
+    def getStudent(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 current_mode(self):
+        certificate = getattr(self.get('studycourse', None), 'certificate', None)
+        if certificate is not None:
+            return certificate.study_mode
+        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.
+    """
+    reg_state = IWorkflowState(student).getState()
+    if reg_state == CLEARANCE:
+        student.clearance_locked = False
+    else:
+        student.clearance_locked = True
+    studycourse = 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 reg_state is None:
+        IWorkflowInfo(student).fireTransition('create')
+    return
+
+@grok.subscribe(IStudent, grok.IObjectRemovedEvent)
+def handle_student_removed(student, event):
+    """If a student is removed a message is logged.
+    """
+    comment = 'Student record removed'
+    target = student.student_id
+    try:
+        grok.getSite()['students'].logger.info('%s - %s' % (
+            target, comment))
+    except KeyError:
+        # If we delete an entire university instance there won't be
+        # a students subcontainer
+        return
+    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.sirp.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.sirp.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%s' % (
+            STUDENT_FILE_STORE_NAME, stud_id[0], 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, SIRPImageFile(
+            file_obj.filename, file_obj.data)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/studycourse.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/studycourse.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/studycourse.py	(revision 7760)
@@ -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
+##
+"""
+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.interface import implementedBy
+from waeup.sirp.students.interfaces import (
+    IStudentStudyCourse, IStudentNavigation, IStudentStudyLevel)
+from waeup.sirp.students.studylevel import CourseTicket
+from waeup.sirp.utils.helpers import attrs_to_fields
+
+class StudentStudyCourse(grok.Container):
+    """This is a container for study levels.
+    """
+    grok.implements(IStudentStudyCourse, IStudentNavigation)
+    grok.provides(IStudentStudyCourse)
+
+    def __init__(self):
+        super(StudentStudyCourse, self).__init__()
+        return
+
+    def getStudent(self):
+        return self.__parent__
+
+    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
+
+        #Create course tickets automatically
+        if cert is not None:
+            for key, val in cert.items():
+                if val.level != studylevel.level:
+                    continue
+                ticket = CourseTicket()
+                ticket.code = val.getCourseCode()
+                ticket.automatic = True
+                ticket.mandatory = val.mandatory
+                ticket.title = val.course.title
+                ticket.fcode = val.course.__parent__.__parent__.__parent__.code
+                ticket.dcode = val.course.__parent__.__parent__.code
+                ticket.credits = val.course.credits
+                ticket.passmark = val.course.passmark
+                ticket.semester = val.course.semester
+                self[str(studylevel.level)][ticket.code] = ticket
+        # 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 co_enabled and studylevel.level % 100 == 0:
+            levels = sorted(self.keys())
+            index = levels.index(str(studylevel.level))
+            if  index > 0:
+                previous_level = self[levels[index-1]]
+                for key, val in previous_level.items():
+                    if val.score < val.passmark:
+                        if key in self[str(studylevel.level)]:
+                            # Carry-over ticket exists
+                            continue
+                        co_ticket = 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
+
+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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/studylevel.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/studylevel.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/studylevel.py	(revision 7760)
@@ -0,0 +1,127 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Container which holds the data of a student study level
+and contains the course tickets.
+"""
+import grok
+from zope.component.interfaces import IFactory
+from waeup.sirp.students.interfaces import (
+    IStudentStudyLevel, IStudentNavigation, ICourseTicket)
+from waeup.sirp.utils.helpers import attrs_to_fields
+from waeup.sirp.students.vocabularies import StudyLevelSource
+
+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
+        self.validation_date = None
+        self.validated_by = None
+        return
+
+    def getStudent(self):
+        return self.__parent__.__parent__
+
+    @property
+    def level_title(self):
+        studylevelsource = StudyLevelSource()
+        return studylevelsource.factory.getTitle(self.__parent__, self.level)
+
+    def addCourseTicket(self, courseticket):
+        """Add a course ticket object.
+        """
+        if not ICourseTicket.providedBy(courseticket):
+            raise TypeError(
+                'StudentStudyLeves contain only ICourseTicket instances')
+        self[courseticket.code] = courseticket
+        return
+
+StudentStudyLevel = attrs_to_fields(StudentStudyLevel)
+
+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
+    course referrer data. If the courses and/or their referrers 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
+        self.title = None
+        self.fcode = None
+        self.dcode = None
+        self.credits = None
+        self.passmark = None
+        self.semester = None
+        return
+
+    def getStudent(self):
+        return self.__parent__.__parent__.__parent__
+
+    def getLevel(self):
+        """Returns the id of the level the ticket has been added to.
+        """
+        return self.__parent__.level
+
+    def getLevelSession(self):
+        """Returns the session of the level the ticket has been added to.
+        """
+        return self.__parent__.level_session
+
+
+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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/__init__.py	(revision 7760)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_courseticket_data.csv
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_courseticket_data.csv	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_courseticket_data.csv	(revision 7760)
@@ -0,0 +1,6 @@
+reg_number,matric_number,level,code,score,mandatory
+1,,100,COURSE1,1,True
+2,,100,COURSE1,2,False
+,100002,100,COURSE1,3,False
+1,,nonsense,COURSE1,5,
+1,,100,NONSENSE,5,
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_payment_data.csv
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_payment_data.csv	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_payment_data.csv	(revision 7760)
@@ -0,0 +1,4 @@
+p_state,p_category,r_pay_reference,r_desc,p_id,reg_number,session_id,r_amount_approved,p_item,amount_auth,r_card_num,r_code,creation_date,type,surcharge_1
+paid,schoolfee,GTB|WEB|FUT|26-11-2010|002295,Approved Successful,3816951290797973744#,1,2010,19500,BTECHBDT,19500,0942,00,2010/11/26 19:59:33.744 GMT+1,online,0
+unpaid,schoolfee,GTB|WEB|FUT|25-11-2010|,Transaction status unconfirmed,3816951290712593757,2,2010,0,BTECHBDT,19500,0942,Z0,2010/11/25 20:16:33.757 GMT+1,online,0
+paid,schoolfee,UBA|WEB|FUT|15-02-2010|000517,Approved Successful,p1266236341955,3,2009,19500,BTECHBDT,19500,0615,00,2010/02/15 13:19:01.955 GMT+1,online,0
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_student_data.csv
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_student_data.csv	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_student_data.csv	(revision 7760)
@@ -0,0 +1,4 @@
+student_id,firstname,lastname,reg_number,date_of_birth,matric_number,email,phone
+X666666,Aaren,Pieri,1,1990-01-02,100000,aa@aa.ng,1234
+,Aaren,Finau,2,1990-01-03,100001,aa@aa.ng,1234
+,Aaren,Berson,3,1990-01-04,100002,aa@aa.ng,1234
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_student_data_migration.csv
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_student_data_migration.csv	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_student_data_migration.csv	(revision 7760)
@@ -0,0 +1,6 @@
+student_id,firstname,lastname,reg_number,date_of_birth,matric_number,email,phone,password,reg_state
+A123456,Aaren,Pieri,1,1990-01-02#,100000,aa@aa.ng,1234,mypw1,clearance started
+B123456,Aaren,Finau,2,1990-01-03,100001,aa@aa.ng,1234,mypw1,cleared
+C123456,Aaren,Berson,3,1990-01-04,100002,aa@aa.ng,1234,mypw1,admitted
+D123456,John,Wolter,4,1990-01-05,100003,aa@aa.ng,1234,mypw1,nonsense
+E123456,John,Kennedy,5,1990-01-06,100004,aa@aa.ng,1234,
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_student_data_update.csv
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_student_data_update.csv	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_student_data_update.csv	(revision 7760)
@@ -0,0 +1,4 @@
+reg_number,date_of_birth,matric_number
+1,1980-01-02,10
+2,1980-01-03,11
+3,1980-01-04,12
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_student_data_update2.csv
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_student_data_update2.csv	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_student_data_update2.csv	(revision 7760)
@@ -0,0 +1,4 @@
+date_of_birth,matric_number
+1970-01-02,100000
+1970-01-03,100001
+1970-01-04,100002
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_studycourse_data.csv
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_studycourse_data.csv	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_studycourse_data.csv	(revision 7760)
@@ -0,0 +1,5 @@
+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
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_studylevel_data.csv
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_studylevel_data.csv	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/sample_studylevel_data.csv	(revision 7760)
@@ -0,0 +1,6 @@
+reg_number,matric_number,level_session,level_verdict,level
+1,,2008,A,100
+2,,2008,B,100
+,100002,2009,C,100
+1,,2008,A,111
+1,,2008,A,nonsense
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_authentication.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_authentication.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_authentication.py	(revision 7760)
@@ -0,0 +1,193 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.security.interfaces import Unauthorized
+from zope.securitypolicy.role import Role
+from zope.securitypolicy.interfaces import IRole, Allow
+from waeup.sirp.authentication import get_principal_role_manager
+from waeup.sirp.interfaces import IAuthPluginUtility, IUserAccount
+from waeup.sirp.students.authentication import (
+    StudentsAuthenticatorSetup, StudentAccount)
+from waeup.sirp.students.tests.test_browser import StudentsFullSetup
+from waeup.sirp.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
+
+
+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 unicode
+        self.assertTrue(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_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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_batching.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_batching.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_batching.py	(revision 7760)
@@ -0,0 +1,653 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 importers.
+"""
+import os
+import shutil
+import tempfile
+import unittest
+import datetime
+from time import time
+from zope.component import createObject
+from zope.component.hooks import setSite, clearSite
+from zope.interface.verify import verifyClass, verifyObject
+
+from waeup.sirp.app import University
+from waeup.sirp.university.faculty import Faculty
+from waeup.sirp.university.department import Department
+from waeup.sirp.students.batching import (
+    StudentProcessor, StudentStudyCourseProcessor,
+    StudentStudyLevelProcessor, CourseTicketProcessor,
+    StudentOnlinePaymentProcessor)
+from waeup.sirp.students.student import Student
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.interfaces import IBatchProcessor
+
+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(',')
+
+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(',')
+
+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(',')
+
+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(',')
+
+class StudentImporterTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentImporterTest, 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'
+        self.app['students'].addStudent(student)
+        self.student = self.app['students'][student.student_id]
+        self.importer = StudentProcessor()
+        self.workdir = tempfile.mkdtemp()
+        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_migration = os.path.join(
+            self.workdir, 'sample_student_data_migration.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_migration, 'wb').write(STUDENT_SAMPLE_DATA_MIGRATION)
+
+    def tearDown(self):
+        super(StudentImporterTest, 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.importer) is True
+        assert verifyClass(
+            IBatchProcessor, StudentProcessor) is True
+
+    def test_parentsExist(self):
+        self.assertFalse(self.importer.parentsExist(None, dict()))
+        self.assertTrue(self.importer.parentsExist(None, self.app))
+
+    def test_entryExists(self):
+        assert self.importer.entryExists(
+            dict(student_id='ID_NONE'), self.app) is False
+        assert self.importer.entryExists(
+            dict(reg_number='123'), self.app) is True
+
+    def test_getParent(self):
+        parent = self.importer.getParent(None, self.app)
+        assert parent is self.app['students']
+
+    def test_getEntry(self):
+        assert self.importer.getEntry(
+            dict(student_id='ID_NONE'), self.app) is None
+        assert self.importer.getEntry(
+            dict(student_id=self.student.student_id), self.app) is self.student
+
+    def test_addEntry(self):
+        new_student = Student()
+        self.importer.addEntry(
+            new_student, dict(), self.app)
+        assert len(self.app['students'].keys()) == 2
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.importer.checkConversion(
+            dict(reg_number='1', reg_state='admitted'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.importer.checkConversion(
+            dict(reg_number='1', reg_state=''))
+        self.assertEqual(len(errs),1)
+        self.assertTrue(('reg_state', 'no value provided') in errs)
+        errs, inv_errs, conv_dict = self.importer.checkConversion(
+            dict(reg_number='1', reg_state='nonsense'))
+        self.assertEqual(len(errs),1)
+        self.assertTrue(('reg_state', 'not allowed') in errs)
+
+    def test_delEntry(self):
+        assert self.student.student_id in self.app['students'].keys()
+        self.importer.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):
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file, STUDENT_HEADER_FIELDS)
+        self.assertEqual(num_warns,0)
+        assert len(self.app['students'].keys()) == 4
+        self.assertEqual(self.app['students']['X666666'].reg_number,'1')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_update(self):
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file, STUDENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file_update, STUDENT_HEADER_FIELDS_UPDATE, 'update')
+        self.assertEqual(num_warns,0)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_update2(self):
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file, STUDENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file_update2, STUDENT_HEADER_FIELDS_UPDATE2, '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.importer.doImport(
+            self.csv_file, STUDENT_HEADER_FIELDS)
+        shutil.rmtree(os.path.dirname(fin_file))
+        num, num_warns, fin_file, fail_file = self.importer.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.importer.doImport(
+            self.csv_file_migration, STUDENT_HEADER_FIELDS_MIGRATION)
+        self.assertEqual(num_warns,2)
+        assert len(self.app['students'].keys()) == 4
+        self.assertTrue('A123456' in self.app['students'].keys())
+        self.assertEqual(self.app['students']['A123456'].state,'clearance started')
+        self.assertEqual(self.app['students']['A123456'].date_of_birth,
+            datetime.date(1990, 1, 2))
+        self.assertFalse(self.app['students']['A123456'].clearance_locked)
+        self.assertEqual(self.app['students']['B123456'].state,'cleared')
+        self.assertEqual(self.app['students']['B123456'].date_of_birth,
+            datetime.date(1990, 1, 3))
+        self.assertTrue(self.app['students']['B123456'].clearance_locked)
+        history = ' '.join(self.app['students']['A123456'].history.messages)
+        self.assertTrue(
+            "State 'clearance started' set by system" in history)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+
+class StudentStudyCourseImporterTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentStudyCourseImporterTest, 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)
+
+        # 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))
+
+        # 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.app['faculties']['fac1'] = Faculty()
+        self.app['faculties']['fac1']['dep1'] = Department()
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate)
+
+        self.importer = StudentStudyCourseProcessor()
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_studycourse_data.csv')
+        open(self.csv_file, 'wb').write(STUDYCOURSE_SAMPLE_DATA)
+        return
+
+    def tearDown(self):
+        super(StudentStudyCourseImporterTest, 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.importer) is True
+        assert verifyClass(
+            IBatchProcessor, StudentStudyCourseProcessor) is True
+
+    def test_entryExists(self):
+        assert self.importer.entryExists(
+            dict(reg_number='REG_NONE'), self.app) is False
+        assert self.importer.entryExists(
+            dict(reg_number='1'), self.app) is True
+
+    def test_getEntry(self):
+        student = self.importer.getEntry(
+            dict(reg_number='1'), self.app).__parent__
+        self.assertEqual(student.reg_number,'1')
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.importer.checkConversion(
+            dict(reg_number='1', certificate='CERT1', current_level='200'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.importer.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.importer.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.importer.checkConversion(
+            dict(reg_number='1', current_level='100'))
+        self.assertEqual(len(errs),0)
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file, STUDYCOURSE_HEADER_FIELDS,'update')
+        self.assertEqual(num_warns,1)
+        studycourse = self.importer.getEntry(dict(reg_number='1'), self.app)
+        self.assertEqual(studycourse.certificate.code, u'CERT1')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+class StudentStudyLevelImporterTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentStudyLevelImporterTest, 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)
+
+        # 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))
+
+        # 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.app['faculties']['fac1'] = Faculty()
+        self.app['faculties']['fac1']['dep1'] = Department()
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate)
+
+        # Update study courses
+        studycourse_file = os.path.join(
+            self.workdir, 'sample_studycourse_data.csv')
+        open(studycourse_file, 'wb').write(STUDYCOURSE_SAMPLE_DATA)
+        importer = StudentStudyCourseProcessor()
+        num, num_warns, fin_file, fail_file = importer.doImport(
+            studycourse_file, STUDYCOURSE_HEADER_FIELDS,'update')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        self.importer = StudentStudyLevelProcessor()
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_studylevel_data.csv')
+        open(self.csv_file, 'wb').write(STUDYLEVEL_SAMPLE_DATA)
+
+    def tearDown(self):
+        super(StudentStudyLevelImporterTest, 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.importer) is True
+        assert verifyClass(
+            IBatchProcessor, StudentStudyLevelProcessor) is True
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.importer.checkConversion(
+            dict(reg_number='1', level='220'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.importer.checkConversion(
+            dict(reg_number='1', level='900'))
+        self.assertEqual(len(errs),1)
+        self.assertTrue(('level','no valid integer') in errs)
+        errs, inv_errs, conv_dict = self.importer.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.importer.doImport(
+            self.csv_file, STUDYLEVEL_HEADER_FIELDS,'create')
+        self.assertEqual(num_warns,2)
+        assert self.importer.entryExists(
+            dict(reg_number='1', level='100'), self.app) is True
+        studylevel = self.importer.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, 'A')
+        self.assertEqual(studylevel.level, 100)
+        shutil.rmtree(os.path.dirname(fin_file))
+        
+
+class CourseTicketImporterTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CourseTicketImporterTest, 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)
+
+        # 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))
+
+        # 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.app['faculties']['fac1'] = Faculty()
+        self.app['faculties']['fac1']['dep1'] = Department()
+        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'].addCourseRef(
+            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)
+        importer = StudentStudyCourseProcessor()
+        num, num_warns, fin_file, fail_file = importer.doImport(
+            studycourse_file, STUDYCOURSE_HEADER_FIELDS,'update')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        # Import study levels
+        importer = 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 = importer.doImport(
+            studylevel_file, STUDYLEVEL_HEADER_FIELDS,'create')
+        shutil.rmtree(os.path.dirname(fin_file))
+
+        self.importer = CourseTicketProcessor()
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_courseticket_data.csv')
+        open(self.csv_file, 'wb').write(COURSETICKET_SAMPLE_DATA)
+
+    def tearDown(self):
+        super(CourseTicketImporterTest, 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.importer) is True
+        assert verifyClass(
+            IBatchProcessor, CourseTicketProcessor) is True
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.importer.checkConversion(
+            dict(reg_number='1', code='COURSE1', level='220'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.importer.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.importer.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'create')
+
+        self.assertEqual(num_warns,2)
+        assert self.importer.entryExists(
+            dict(reg_number='1', level='100', code='COURSE1'), self.app) is True
+        courseticket = self.importer.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)
+        shutil.rmtree(os.path.dirname(fin_file))
+
+class PaymentImporterTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(PaymentImporterTest, 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)
+
+        # 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 = 'p123'
+        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.importer = StudentOnlinePaymentProcessor()
+        self.csv_file = os.path.join(
+            self.workdir, 'sample_payment_data.csv')
+        open(self.csv_file, 'wb').write(PAYMENT_SAMPLE_DATA)
+
+    def tearDown(self):
+        super(PaymentImporterTest, 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.importer) is True
+        assert verifyClass(
+            IBatchProcessor, StudentOnlinePaymentProcessor) is True
+
+    def test_getEntry(self):
+        assert self.importer.getEntry(
+            dict(student_id='ID_NONE', p_id='nonsense'), self.app) is None
+        assert self.importer.getEntry(
+            dict(student_id=self.student.student_id, p_id='p123'),
+            self.app) is self.student['payments']['p123']
+        assert self.importer.getEntry(
+            dict(student_id=self.student.student_id, p_id='XXXXXX123'),
+            self.app) is self.student['payments']['p123']
+
+    def test_addEntry(self):
+        self.assertEqual(len(self.student['payments'].keys()),1)
+        payment1 = createObject(u'waeup.StudentOnlinePayment')
+        payment1.p_id = 'p234'
+        self.importer.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'
+        self.importer.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']['p456'].p_id, 'p456')
+
+    def test_checkConversion(self):
+        errs, inv_errs, conv_dict = self.importer.checkConversion(
+            dict(reg_number='1', p_id='3816951266236341955'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.importer.checkConversion(
+            dict(reg_number='1', p_id='p1266236341955'))
+        self.assertEqual(len(errs),0)
+        errs, inv_errs, conv_dict = self.importer.checkConversion(
+            dict(reg_number='1', p_id='nonsense'))
+        self.assertEqual(len(errs),1)
+        timestamp = "%d" % int(time()*1000)
+        p_id = "p%s" % timestamp
+        errs, inv_errs, conv_dict = self.importer.checkConversion(
+            dict(reg_number='1', p_id=p_id))
+        self.assertEqual(len(errs),0)
+
+    def test_import(self):
+        num, num_warns, fin_file, fail_file = self.importer.doImport(
+            self.csv_file, PAYMENT_HEADER_FIELDS,'create')
+        self.assertEqual(num_warns,0)
+        payment = self.importer.getEntry(dict(reg_number='1',
+            p_id='p1290797973744'), self.app)
+        self.assertEqual(payment.p_id, 'p1290797973744')
+        cdate = payment.creation_date.strftime("%Y-%m-%d %H:%M:%S")
+        self.assertEqual(cdate, "2010-11-26 19:59:33")
+        shutil.rmtree(os.path.dirname(fin_file))
+
+def test_suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+        StudentImporterTest,StudentStudyCourseImporterTest,
+        StudentStudyLevelImporterTest,CourseTicketImporterTest,
+        PaymentImporterTest,]:
+        suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+                testcase
+                )
+        )
+    return suite
+
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_browser.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_browser.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_browser.py	(revision 7760)
@@ -0,0 +1,1667 @@
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public 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
+from StringIO import StringIO
+from datetime import datetime
+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 zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.testbrowser.testing import Browser
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.app import University
+from waeup.sirp.configuration import SessionConfiguration
+from waeup.sirp.students.student import Student
+from waeup.sirp.students.studylevel import StudentStudyLevel
+from waeup.sirp.university.faculty import Faculty
+from waeup.sirp.university.department import Department
+from waeup.sirp.interfaces import IUserAccount
+from waeup.sirp.authentication import LocalRoleSetEvent
+from waeup.sirp.hostels.hostel import Hostel, Bed, NOT_OCCUPIED
+
+PH_LEN = 2059  # Length of placeholder file
+
+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 = 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'
+        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.clearance_student_path = self.student_path + '/view_clearance'
+        self.personal_student_path = self.student_path + '/view_personal'
+        self.edit_clearance_student_path = self.student_path + '/edit_clearance'
+        self.edit_personal_student_path = self.student_path + '/edit_personal'
+        self.studycourse_student_path = self.student_path + '/studycourse'
+        self.payments_student_path = self.student_path + '/payments'
+        self.acco_student_path = self.student_path + '/accommodation'
+        self.history_student_path = self.student_path + '/history'
+
+        # Create 5 access codes with prefix'PWD'
+        pin_container = self.app['accesscodes']
+        pin_container.createBatch(
+            datetime.now(), '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.app['faculties']['fac1'] = Faculty()
+        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 = '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'].addCourseRef(
+            self.course, level=100)
+
+        # Configure university
+        self.app['configuration'].accommodation_states = ['admitted']
+        self.app['configuration'].accommodation_session = 2004
+        self.app['configuration'].carry_over = True
+        configuration = SessionConfiguration()
+        # These attributes must also exist in the customization packages.
+        configuration.academic_session = 2004
+        configuration.fee_1 = 20000
+        configuration.boocking_fee = 500
+        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'
+        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 = '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("Search").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("Search").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("Search").click()
+        self.assertTrue('Anna Tester' 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("Search").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("Search").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("Search").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("Search").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("Search").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("Search").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("Search").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("Search").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("Search").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("Search").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("Search").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+        # The old searchterm will be used again
+        self.browser.getControl("Search").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("Search").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("Search").click()
+        self.assertTrue('No student found' in self.browser.contents)
+        return
+
+class StudentUITests(StudentsFullSetup):
+    # Tests for Student class views and pages
+
+    layer = FunctionalLayer
+
+    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_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("Manage").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.manage_student_path)
+        # Managers can edit base data and fire transitions
+        self.browser.getControl(name="transition").value = ['admit']
+        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_student_path)
+        self.browser.getLink("Manage").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.edit_clearance_student_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_student_path)
+        self.browser.getLink("Edit").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.edit_personal_student_path)
+        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_student_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_student_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_student_path)
+        self.assertMatches('...Student 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.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('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 class="widget">Nothing</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.edit_clearance_student_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.edit_clearance_student_path)
+        pseudo_image = StringIO('I pretend to be a graphics file')
+        ctrl = self.browser.getControl(name='birthcertificateupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_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
+        pseudo_image = StringIO('I pretend to be a graphics file')
+        ctrl = self.browser.getControl(name='birthcertificateupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_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), 31)
+        # Reuploading a file which is bigger than 150k will raise an error
+        self.browser.open(self.edit_clearance_student_path)
+        photo_content = 'A' * 1024 * 151  # A string of 21 KB size
+        pseudo_image = StringIO(photo_content)
+        ctrl = self.browser.getControl(name='birthcertificateupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='my_birth_certificate.jpg')
+        self.browser.getControl(
+            name='upload_birthcertificateupload').click()
+        self.assertTrue(
+            'Uploaded file is too big' in self.browser.contents)
+        # File names must meet several conditions
+        pseudo_image = StringIO('I pretend to be a graphics file')
+        ctrl = self.browser.getControl(name='birthcertificateupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='my.photo.jpg')
+        self.browser.getControl(
+            name='upload_birthcertificateupload').click()
+        self.assertTrue('File name contains more than one dot'
+            in self.browser.contents)
+        ctrl = self.browser.getControl(name='birthcertificateupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='my_birth_certificate')
+        self.browser.getControl(
+            name='upload_birthcertificateupload').click()
+        self.assertTrue('File name has no extension' in self.browser.contents)
+        ctrl = self.browser.getControl(name='birthcertificateupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='my_birth_certificate.bmp')
+        self.browser.getControl(
+            name='upload_birthcertificateupload').click()
+        self.assertTrue('Only the following extension 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 add and delete second file
+        self.browser.open(self.edit_clearance_student_path)
+        pseudo_image = StringIO('I pretend to be a graphics file')
+        ctrl = self.browser.getControl(name='birthcertificateupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='my_acceptance_letter.jpg')
+        self.browser.getControl(
+            name='upload_acceptanceletterupload').click()
+        self.assertFalse(
+            '<a target="image" href="acceptance_letter">'
+            in self.browser.contents)
+        ctrl = self.browser.getControl(name='acceptanceletterupload')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_image, filename='my_acceptance_letter.jpg')
+        self.browser.getControl(
+            name='upload_acceptanceletterupload').click()
+        self.assertTrue(
+            '<a target="image" href="acceptance_letter">'
+            in self.browser.contents)
+        self.browser.getControl(
+            name='delete_acceptanceletterupload').click()
+        self.assertTrue(
+            'acceptance_letter deleted'
+            in self.browser.contents)
+        # Managers can upload a file via the StudentBaseManageFormPage
+        self.browser.open(self.manage_student_path)
+        pseudo_image = StringIO('I pretend to be a graphics file')
+        ctrl = self.browser.getControl(name='passportuploadmanage')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_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
+        file_ctrl.add_file(pseudo_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)
+        # The clearance slip can't be opened because it requires a proper
+        # passport jpg file.
+        self.browser.open(self.student_path + '/clearance.pdf')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertTrue('Error in image file' 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.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_student_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("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('...<span>100</span>...', self.browser.contents)
+        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("Remove selected").click()
+        self.assertMatches('...Successfully removed...', self.browser.contents)
+        # Add level again
+        self.browser.getControl(name="addlevel").value = ['100']
+        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)
+        self.browser.getControl("Add course ticket").click()
+        self.browser.getControl(name="form.course").value = ['COURSE1']
+        self.browser.getControl("Add course ticket").click()
+        self.assertTrue('Successfully added' in self.browser.contents)
+        self.browser.getControl("Add course ticket").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(name="form.score").value = '10'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved' in self.browser.contents)
+        # Carry-over courses will be collected when next level is created
+        self.browser.open(self.student_path + '/studycourse/manage')
+        # Add next level
+        self.browser.getControl(name="addlevel").value = ['200']
+        self.browser.getControl("Add study level").click()
+        self.browser.getLink("200").click()
+        self.assertMatches(
+            '...: Study Level 200 (Year 2)...', self.browser.contents)
+        # COURSE1 has score 0 and thus will become 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)
+        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.manage_student_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()
+        self.browser.getControl(
+            name="transition").value = ['pay_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 = ['pay_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 = 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_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,password
+Aaren,Pieri,1,1990-01-02,100000,aa@aa.ng,1234,mypwd1
+Claus,Finau,2,1990-01-03,100001,aa@aa.ng,1234,mypwd1
+Brit,Berson,3,1990-01-04,100001,aa@aa.ng,1234,mypwd1
+""")
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(datacenter_path)
+        self.browser.getLink('Upload CSV file').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('Batch processing').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 Importer').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 CSV file').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('Batch processing').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 Importer (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("Search").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("Search").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_student_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.clearance_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)
+        # Only in state clearance requested the CO does see the 'Clear' button
+        self.browser.open(self.clearance_student_path)
+        self.assertFalse('Clear student' in self.browser.contents)
+        IWorkflowInfo(self.student).fireTransition('request_clearance')
+        self.browser.open(self.clearance_student_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.getLink("Reject clearance").click()
+        self.assertTrue('Clearance has been annulled' in self.browser.contents)
+        urlmessage = 'Clearance+has+been+annulled.'
+        # CO does now see the contact form
+        self.assertEqual(self.browser.url, self.student_path +
+            '/contactstudent?subject=%s' % urlmessage)
+        self.assertTrue('clearance started' in self.browser.contents)
+        IWorkflowInfo(self.student).fireTransition('request_clearance')
+        self.browser.open(self.clearance_student_path)
+        self.browser.getLink("Reject clearance").click()
+        self.assertTrue('Clearance request has been rejected'
+            in self.browser.contents)
+        self.assertTrue('clearance started' in self.browser.contents)
+        # CO does now also 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 = 'Clearance rejected'
+        self.browser.getControl("Send message now").click()
+        self.assertTrue('Your message has been sent' 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.assertTrue(self.student_id in self.browser.contents)
+
+    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 = '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_student_path + '/110'
+        # 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
+        self.browser.open(L110_student_path)
+        self.assertFalse('Validate' in self.browser.contents)
+        IWorkflowInfo(self.student).fireTransition('register_courses')
+        self.browser.open(L110_student_path)
+        self.assertFalse('Validate' in self.browser.contents)
+        self.student['studycourse'].current_level = 110
+        self.browser.open(L110_student_path)
+        self.assertTrue('Validate' in self.browser.contents)
+        # ... but a 100L CA does not see the button on other levels
+        studylevel2 = StudentStudyLevel()
+        studylevel2.level = 200
+        self.student['studycourse'].addStudentStudyLevel(
+            cert,studylevel2)
+        L200_student_path = self.studycourse_student_path + '/200'
+        self.browser.open(L200_student_path)
+        self.assertFalse('Validate' 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.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)
+        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 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 throug 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.assertTrue(self.student_id in self.browser.contents)
+
+    def test_student_change_password(self):
+        # Students can change the 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.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_access(self):
+        # Students can access their own objects
+        # and can perform actions
+        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 upload a passport picture
+        self.browser.open(self.student_path + '/change_portrait')
+        ctrl = self.browser.getControl(name='passportuploadedit')
+        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(
+            name='upload_passportuploadedit').click()
+        self.assertTrue(
+            '<img align="middle" height="125px" src="passport.jpg" />'
+            in self.browser.contents)
+        # Student can view the clearance data
+        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.student.email = 'aa@aa.ng'
+        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
+        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()
+        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)
+        # 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
+        IWorkflowInfo(self.student).fireTransition('clear')
+        IWorkflowInfo(self.student).fireTransition('pay_first_school_fee')
+        # 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()
+        self.browser.getLink("100").click()
+        self.browser.getLink("Add and remove courses").click()
+        self.browser.getControl("Add course ticket").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("Add and remove courses").click()
+        self.browser.getControl("Add course ticket").click()
+        self.browser.getControl(name="form.course").value = ['COURSE1']
+        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')
+        # Now the student can 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)
+        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')
+        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_student_path)
+        self.browser.getControl("Add online 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_url = self.browser.url
+
+        # The pdf payment receipt can't yet be opened
+        self.browser.open(payment_url + '/payment_receipt.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_student_path)
+        self.browser.getControl("Add online 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)
+
+        # Managers can open the callback view which simulates a valid callback
+        self.assertEqual(len(self.app['accesscodes']['SFE-0']),0)
+        self.browser.open(payment_url)
+        self.browser.getLink("Request callback").click()
+        self.assertMatches('...Valid callback received...',
+                          self.browser.contents)
+
+        # Callback can't be applied twice
+        self.browser.open(payment_url + '/callback')
+        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_student_path)
+        self.browser.getControl("Add online 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 receipt
+        self.browser.open(payment_url)
+        self.browser.getLink("Download payment receipt").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_student_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_student_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 open the callback view which simulates a valid callback
+        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 + '/callback')
+        self.assertMatches('...Valid callback received...',
+                          self.browser.contents)
+        expected = '''...
+        <td>
+          Paid
+        </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]
+        ac.owner = self.student_id
+        return
+
+    def test_student_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()
+
+        # Students can add online clearance payment tickets
+        self.browser.open(self.payments_student_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 open the callback view which simulates a valid callback
+        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.browser.open(payment_url + '/callback')
+        self.assertMatches('...Valid callback received...',
+                          self.browser.contents)
+        expected = '''...
+        <td>
+          Paid
+        </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]
+        ac.owner = self.student_id
+
+        # Students can open the pdf payment receipt
+        self.browser.open(payment_url + '/payment_receipt.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')
+        pseudo_image = StringIO('I pretend to be a graphics file')
+        ctrl = self.browser.getControl(name='passportuploadedit')
+        file_ctrl = ctrl.mech_control
+        file_ctrl.add_file(pseudo_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)
+
+        # Students can add online school fee payment tickets
+        self.browser.open(self.payments_student_path)
+        self.browser.getControl("Add online 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)
+
+        # Students can open the callback view which simulates a valid callback
+        self.assertEqual(len(self.app['accesscodes']['SFE-0']),0)
+        self.browser.open(self.browser.url + '/callback')
+        self.assertMatches('...Valid callback received...',
+                          self.browser.contents)
+
+        # Students can remove only online payment tickets which have
+        # not received a valid callback
+        self.browser.open(self.payments_student_path)
+        self.assertRaises(
+            LookupError, self.browser.getControl, name='val_id')
+        self.browser.open(self.payments_student_path + '/addop')
+        self.browser.getControl(name="form.p_category").value = ['gown']
+        self.browser.getControl("Create ticket").click()
+        self.browser.open(self.payments_student_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)
+
+        # The new SFE-0 pin can be used for starting course registration
+        IWorkflowInfo(self.student).fireTransition('request_clearance')
+        IWorkflowInfo(self.student).fireTransition('clear')
+        self.browser.open(self.studycourse_student_path)
+        self.browser.getLink('Start course registration').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 course registration now").click()
+        self.assertMatches('...Course registration has been started...',
+                           self.browser.contents)
+        self.assertTrue(self.student.state == 'school fee paid')
+        return
+
+    def test_manage_accommodation(self):
+        # 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_student_path)
+        self.browser.getControl("Add online 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['configuration'].accommodation_session = 2011
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...Your current session does not match...',
+                           self.browser.contents)
+        self.app['configuration'].accommodation_session = 2004
+        self.browser.getControl("Add online 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 + '/callback')
+        # 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]
+        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_student_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_student_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)
+        # 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')
+        # 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_student_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_student_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_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.getControl("Add online 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 + '/callback')
+        # 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]
+        ac.owner = u'Anybody'
+        parts = pin.split('-')[1:]
+        sfeseries, sfenumber = parts
+
+        # Students can use HOS code and book a bed space with it
+        self.browser.open(self.acco_student_path)
+        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)
+        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)
+        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_student_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.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_student_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_student_path)
+        self.assertFalse('Remove' in self.browser.contents)
+        self.assertFalse('val_id' in self.browser.contents)
+        return
+
+    def test_change_password_request(self):
+        self.browser.open('http://localhost/app/changepw')
+        self.browser.getControl(name="form.reg_number").value = '123'
+        self.browser.getControl(name="form.email").value = 'aa@aa.ng'
+        self.browser.getControl("Get new login credentials").click()
+        self.assertTrue('An email with' in self.browser.contents)
+
+    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)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_catalog.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_catalog.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_catalog.py	(revision 7760)
@@ -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
+##
+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.sirp.app import University
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.students.student import Student
+from waeup.sirp.students.studylevel import StudentStudyLevel, CourseTicket
+from waeup.sirp.university.faculty import Faculty
+from waeup.sirp.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 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 = createObject('waeup.Certificate')
+        self.certificate.code = u'CERT1'
+        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)
+
+        # Create student with subobjects
+        student = Student()
+        student.firstname = u'Bob'
+        student.lastname = u'Tester'
+        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
+        # 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'
+        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'
+        payment.r_pay_reference = u'GTB|WEB|FUT'
+        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]
+
+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_search_by_reference(self):
+        # We can find a payment ticket by parts of the response reference
+        cat = queryUtility(ICatalog, name='payments_catalog')
+        results = cat.searchResults(r_pay_reference='GTB')
+        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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_container.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_container.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_container.py	(revision 7760)
@@ -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.sirp.app import University
+from waeup.sirp.university.department import Department
+from waeup.sirp.students.interfaces import (
+    IStudentsContainer,
+    )
+from waeup.sirp.students.container import (
+    StudentsContainer, 
+    )
+from waeup.sirp.testing import (
+    FunctionalLayer, FunctionalTestCase, remove_logger)
+
+class StudentsContainerTestCase(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        remove_logger('waeup.sirp.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.sirp.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.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_dynamicroles.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_dynamicroles.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_dynamicroles.py	(revision 7760)
@@ -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
+##
+"""
+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.sirp.testing import FunctionalLayer
+from waeup.sirp.students.tests.test_browser import StudentsFullSetup
+from waeup.sirp.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()
+        self.officer_role = 'waeup.StudentsClearanceOfficer'
+        # assign clearance permissions for a virtual officer
+        prm = IPrincipalRoleManager(self.app['faculties']['fac1']['dep1'])
+        prm.assignRoleToPrincipal('waeup.local.ClearanceOfficer', 'alice')
+        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('bob')
+        self.assertEqual(result, [])
+        return
+
+    def test_valid_officer(self):
+        # for an officer that has clearance role on the connected dept
+        # we get the ApplicationsOfficer role on our student
+        prm = IPrincipalRoleManager(self.student)
+        result = prm.getRolesForPrincipal('alice')
+        self.assertEqual(result, [(self.officer_role, Allow)])
+        return
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_payment.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_payment.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_payment.py	(revision 7760)
@@ -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
+##
+"""Tests for student payments.
+
+"""
+
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.students.payments import (
+    StudentOnlinePayment, StudentOnlinePaymentFactory)
+from waeup.sirp.payments.interfaces import IOnlinePayment
+from waeup.sirp.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)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_student.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_student.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_student.py	(revision 7760)
@@ -0,0 +1,100 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Tests for students and related.
+"""
+from zope.component.interfaces import IFactory
+from zope.interface import verify
+from waeup.sirp.students.student import Student, StudentFactory
+from waeup.sirp.students.studycourse import StudentStudyCourse
+from waeup.sirp.students.studylevel import StudentStudyLevel, CourseTicket
+from waeup.sirp.students.payments import StudentPaymentsContainer
+from waeup.sirp.students.accommodation import StudentAccommodation, BedTicket
+from waeup.sirp.applicants.interfaces import IApplicantBaseData
+from waeup.sirp.students.interfaces import (
+    IStudent, IStudentStudyCourse, IStudentPaymentsContainer, IStudentAccommodation,
+    IStudentStudyLevel, ICourseTicket, IBedTicket)
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.university.department import Department
+
+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.verifyObject(IStudent, self.student)
+        verify.verifyClass(IStudentStudyCourse, StudentStudyCourse)
+        verify.verifyObject(IStudentStudyCourse, self.studycourse)
+        verify.verifyObject(IStudentStudyLevel, self.studylevel)
+        verify.verifyObject(ICourseTicket, self.courseticket)
+        verify.verifyClass(IStudentPaymentsContainer, StudentPaymentsContainer)
+        verify.verifyObject(IStudentPaymentsContainer, self.payments)
+        verify.verifyClass(IStudentAccommodation, StudentAccommodation)
+        verify.verifyObject(IStudentAccommodation, self.accommodation)
+        verify.verifyClass(IBedTicket, BedTicket)
+        verify.verifyObject(IBedTicket, 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)
+
+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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_utils.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_utils.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_utils.py	(revision 7760)
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+import unittest
+from waeup.sirp.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', size=12)
+        self.assertTrue(isinstance(result1, unicode))
+        self.assertEqual(
+            result1, u'<font color="black" size="10">sample</font>')
+        self.assertEqual(
+            result2, u'<font color="black" size="10">ümlaut</font>')
+        self.assertEqual(
+            result3, u'<font color="black" size="10">3</font>')
+        self.assertEqual(
+            result4, u'<font color="black" size="10">unicöde</font>')
+        self.assertEqual(
+            result5, u'<font color="red" size="12">sample</font>')
+        return
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_vocabularies.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_vocabularies.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/tests/test_vocabularies.py	(revision 7760)
@@ -0,0 +1,69 @@
+## $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.sirp.students.vocabularies import StudyLevelSource
+from waeup.sirp.university.certificate import Certificate
+
+
+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])
+        # 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 change the certificates start_level to pre-studies
+        studycourse.certificate.start_level = 10
+        self.assertEqual(studylevelsource.getTitle(studycourse, 10),
+            'Pre-Studies')
+        # Repeating Pre-Studies level does not exist
+        self.assertRaises(KeyError,studylevelsource.getTitle,studycourse,20)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/utils.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/utils.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/utils.py	(revision 7760)
@@ -0,0 +1,390 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 random import SystemRandom as r
+from datetime import datetime
+from zope.i18n import translate
+from zope.component import getUtility
+from reportlab.pdfgen import canvas
+from reportlab.lib import colors
+from reportlab.lib.units import cm
+from reportlab.lib.enums import TA_RIGHT
+from reportlab.lib.pagesizes import A4
+from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
+from reportlab.platypus import (Frame, Paragraph, Image,
+    Table, Spacer)
+from reportlab.platypus.tables import TableStyle
+from reportlab.platypus.flowables import PageBreak
+from zope.component import getUtility
+from zope.formlib.form import setUpEditWidgets
+from waeup.sirp.interfaces import IExtFileStore, ISIRPUtils
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.students.interfaces import IStudentsUtils
+
+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),
+    ]
+
+FONT_SIZE = 10
+FONT_COLOR = 'black'
+
+def formatted_label(color=FONT_COLOR, size=FONT_SIZE):
+    tag1 ='<font color=%s size=%d>' % (color, size)
+    return tag1 + '%s:</font>'
+
+def formatted_text(text, color=FONT_COLOR, size=FONT_SIZE):
+    """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.).
+
+    The returned snippet is unicode type.
+    """
+    if not isinstance(text, unicode):
+        if isinstance(text, basestring):
+            text = text.decode('utf-8')
+        else:
+            text = unicode(text)
+    tag1 = u'<font color="%s" size="%d">' % (color, size)
+    return tag1 + u'%s</font>' % text
+
+def generate_student_id(students,letter):
+    if letter == '?':
+        letter= r().choice('ABCDEFGHKLMNPQRSTUVWXY')
+    sid = u"%c%d" % (letter,r().randint(99999,1000000))
+    while sid in students.keys():
+        sid = u"%c%d" % (letter,r().randint(99999,1000000))
+    return sid
+
+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):
+    """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.sirp.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)])
+    portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+    for widget in studentview.widgets:
+        if widget.name == 'form.adm_code':
+            continue
+        f_label = formatted_label(size=12) % translate(
+            widget.label.strip(), 'waeup.sirp',
+            target_language=portal_language)
+        f_label = Paragraph(f_label, style["Normal"])
+        f_text = formatted_text(widget(), size=12)
+        f_text = Paragraph(f_text, style["Normal"])
+        data_right.append([f_label,f_text])
+    table_left = Table(data_left,style=SLIP_STYLE)
+    table_right = Table(data_right,style=SLIP_STYLE, colWidths=[6*cm, 8*cm])
+    table = Table([[table_left, table_right],],style=SLIP_STYLE)
+    return table
+
+def render_table_data(tableheader,tabledata):
+    """Render children table for an existing frame.
+    """
+    data = []
+    #data.append([Spacer(1, 12)])
+    line = []
+    style = getSampleStyleSheet()
+    for element in tableheader:
+        field = formatted_text(element[0], color='white')
+        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 insert_footer(pdf,width,style,text=None, number_of_pages=1):
+      """Render the whole footer frame.
+      """
+      story = []
+      frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
+      timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
+      left_text = '<font size=10>%s</font>' % timestamp
+      story.append(Paragraph(left_text, style["Normal"]))
+      frame_footer.addFromList(story,pdf)
+      story = []
+      frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
+      portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+      right_text = translate(_('<font size=10>${a} Page ${b} of ${c}</font>',
+          mapping = {'a':text, 'b':pdf.getPageNumber(), 'c':number_of_pages}),
+          'waeup.sirp', target_language=portal_language)
+      story.append(Paragraph(right_text, style["Right"]))
+      frame_footer.addFromList(story,pdf)
+
+class StudentsUtils(grok.GlobalUtility):
+    """A collection of methods subject to customization.
+    """
+    grok.implements(IStudentsUtils)
+
+    def setReturningData(self, student):
+        student['studycourse'].current_level += 100
+        student['studycourse'].current_session += 1
+        verdict = student['studycourse'].current_verdict
+        student['studycourse'].current_verdict = '0'
+        student['studycourse'].previous_verdict = verdict
+        return
+
+    def getPaymentDetails(self, category, student):
+        d = {}
+        d['p_item'] = u''
+        d['amount'] = 0
+        d['error'] = u''
+        d['p_session'] = student['studycourse'].current_session
+        session = str(d['p_session'])
+        try:
+            academic_session = grok.getSite()['configuration'][session]
+        except KeyError:
+            d['error'] = u'Session configuration object is not available.'
+            return d
+        d['surcharge_1'] = academic_session.surcharge_1
+        d['surcharge_2'] = academic_session.surcharge_2
+        d['surcharge_3'] = academic_session.surcharge_3
+        if category == 'schoolfee':
+            d['amount'] = academic_session.school_fee_base
+            d['p_item'] = student['studycourse'].certificate.code
+        elif category == 'clearance':
+            d['p_item'] = student['studycourse'].certificate.code
+            d['amount'] = academic_session.clearance_fee
+        elif category == 'bed_allocation':
+            d['p_item'] = self.getAccommodationDetails(student)['bt']
+            d['amount'] = academic_session.booking_fee
+        return d
+
+    def getAccommodationDetails(self, student):
+        d = {}
+        d['error'] = u''
+        site_configuration = grok.getSite()['configuration']
+        d['booking_session'] = site_configuration.accommodation_session
+        d['allowed_states'] = site_configuration.accommodation_states
+        # Determine bed type
+        studycourse = student['studycourse']
+        certificate = getattr(studycourse,'certificate',None)
+        entry_session = studycourse.entry_session
+        current_level = studycourse.current_level
+        if not (entry_session and current_level and certificate):
+            return
+        end_level = certificate.end_level
+        if entry_session == grok.getSite()['configuration'].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
+
+    # In the standard configuration we select the first bed found,
+    # but can also randomize the selection if we like.
+    def selectBed(self, available_beds):
+        return available_beds[0]
+
+    def renderPDF(self, view, filename='slip.pdf',
+        student=None, studentview=None, tableheader=None, tabledata=None):
+        # (0,0),(-1,-1) = whole table
+        # (0,0),(0,-1) = first column
+        # (-1,0),(-1,-1) = last column
+        # (0,0),(-1,0) = first row
+        # (0,-1),(-1,-1) = last row
+
+        pdf = canvas.Canvas(filename,pagesize=A4)
+        pdf.setTitle(view.label)
+        pdf.setSubject(view.title)
+        pdf.setAuthor('%s (%s)' % (view.request.principal.title,
+            view.request.principal.id))
+        pdf.setCreator('WAeUP SIRP')
+        width, height = A4
+        footer_text = view.label
+        if getattr(student, 'student_id', None) is not None:
+            footer_text = "%s - %s - " % (student.student_id, footer_text)
+        style = getSampleStyleSheet()
+        style.add(ParagraphStyle(name='Right', alignment=TA_RIGHT))
+        pdf.line(1*cm,height-(1.8*cm),width-(1*cm),height-(1.8*cm))
+
+        story = []
+        frame_header = Frame(1*cm,1*cm,width-(1.7*cm),height-(1.7*cm))
+        header_title = getattr(grok.getSite(), 'name', u'Sample University')
+        story.append(Paragraph(header_title, style["Heading1"]))
+        frame_header.addFromList(story,pdf)
+
+        # Insert student data table
+        story = []
+        frame_body = Frame(1*cm,1*cm,width-(2*cm),height-(3.5*cm))
+        story.append(Paragraph(view.label, style["Heading2"]))
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        if student is not None:
+            #story.append(Spacer(1, 12))
+            bd_translation = translate(_('Base Data'),
+                'waeup.sirp', target_language=portal_language)
+            story.append(Paragraph(bd_translation, style["Heading3"]))
+            studenttable = render_student_data(studentview)
+            story.append(studenttable)
+
+        # Insert widgets
+        story.append(Paragraph(view.title, style["Heading3"]))
+        set_up_widgets(view)
+        data = []
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        for widget in view.widgets:
+            f_label = '<font size=12>%s</font>:' % translate(
+                widget.label.strip(), 'waeup.sirp',
+                target_language=portal_language)
+            f_label = Paragraph(f_label, style["Normal"])
+            f_text = formatted_text(widget(), size=12)
+            f_text = Paragraph(f_text, style["Normal"])
+            data.append([f_label,f_text])
+        table = Table(data,style=SLIP_STYLE, colWidths=[5*cm, 13.5*cm])
+        story.append(table)
+
+        # Import browser components locally
+        # to avoid circular import
+        from waeup.sirp.students.viewlets import FileManager
+        from waeup.sirp.browser import DEFAULT_IMAGE_PATH
+        data = []
+        # Collect viewlets
+        fm = FileManager(view.context, view.request, view)
+        fm.update()
+        if fm.viewlets:
+            sc_translation = translate(_('Scanned Documents'), 'waeup.sirp',
+                target_language=portal_language)
+            story.append(Paragraph(sc_translation, style["Heading3"]))
+            # Insert list of scanned documents
+            for viewlet in fm.viewlets:
+                f_label = formatted_label(size=12) % translate(
+                viewlet.label, 'waeup.sirp',
+                target_language=portal_language)
+                f_label = Paragraph(f_label, style["Normal"])
+                if viewlet.template.__grok_name__ == 'imagedisplay':
+                    # In case we define FileDisplay viewlets with
+                    # an imagedisplay template in the customization package
+                    img = getUtility(IExtFileStore).getFileByContext(
+                        view.context, attr=viewlet.download_name)
+                    if img is None:
+                        img = open(DEFAULT_IMAGE_PATH, 'rb')
+                    doc_img = Image(img.name, width=2*cm, height=1*cm, kind='bound')
+                    data.append([f_label,doc_img])
+                else:
+                    f_text = formatted_text(viewlet.title, size=12)
+                    f_text = Paragraph(f_text, style["Normal"])
+                    data.append([f_label,f_text])
+            table = Table(data,style=SLIP_STYLE, colWidths=[5*cm, 13.5*cm])
+            story.append(table)
+
+        try:
+            frame_body.addFromList(story,pdf)
+        except IOError:
+            view.flash('Error in image file.')
+            return view.redirect(view.url(view.context))
+
+        if tabledata and tableheader:
+            insert_footer(pdf,width,style,footer_text,number_of_pages=2)
+        else:
+            insert_footer(pdf,width,style,footer_text)
+
+        # Insert content table on second page
+        if tabledata and tableheader:
+            pdf.showPage()
+            frame_body = Frame(1*cm,1*cm,width-(2*cm),height-(2*cm))
+            story = []
+            story.append(Paragraph(view.content_title, style["Heading3"]))
+            #story.append(Spacer(1, 12))
+            contenttable = render_table_data(tableheader,tabledata)
+            story.append(contenttable)
+            frame_body.addFromList(story,pdf)
+            insert_footer(pdf,width,style,footer_text,number_of_pages=2)
+
+        view.response.setHeader(
+            'Content-Type', 'application/pdf')
+        return pdf.getpdfdata()
+
+    def getVerdictsDict(self):
+        """Provide a dictionary of verdicts.
+        """
+        return {
+            '0': 'not yet',
+            'A': 'Successful student',
+            'B': 'Student with carryover courses',
+            'C': 'Student on probation',
+            'D': 'Withdrawn from the faculty',
+            'E': 'Student who were previously on probation',
+            'F': 'Medical case',
+            'G': 'Absent from examination',
+            'H': 'Withheld results',
+            'I': 'Expelled/rusticated/suspended student',
+            'J': 'Temporary withdrawn from the university',
+            'K': 'Unregistered student',
+            'L': 'Referred student',
+            'M': 'Reinstatement',
+            'N': 'Student on transfer',
+            'O': 'NCE-III repeater',
+            'Y': 'No previous verdict',
+            'X': 'New 300 level student',
+            'Z': 'Successful student (provisional)',
+            'A1': 'First Class',
+            'A2': 'Second Class Upper',
+            'A3': 'Second Class Lower',
+            'A4': 'Third Class',
+            'A5': 'Pass',
+            'A6': 'Distinction',
+            'A7': 'Credit',
+            'A8': 'Merit',
+            }
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/viewlets.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/viewlets.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/viewlets.py	(revision 7760)
@@ -0,0 +1,793 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.interfaces import (
+    ISIRPObject, IExtFileStore, IFileStoreNameChooser, ISIRPUtils)
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.utils.helpers import string_from_bytes, file_size
+from waeup.sirp.browser import DEFAULT_IMAGE_PATH
+from waeup.sirp.browser.viewlets import (
+    PrimaryNavTab, ManageActionButton, AddActionButton)
+from waeup.sirp.browser.layout import default_primary_nav_template
+from waeup.sirp.students.workflow import (ADMITTED, PAID,
+    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED)
+from waeup.sirp.students.browser import (
+    StudentClearanceManageFormPage,
+    write_log_message, StudentBaseManageFormPage,
+    StudentFilesUploadPage, ExportPDFClearanceSlipPage, StudentsContainerPage,
+    StudentsContainerManagePage, StudentBaseDisplayFormPage,
+    StudentClearanceDisplayFormPage, StudentPersonalDisplayFormPage,
+    StudyCourseDisplayFormPage, StudyLevelDisplayFormPage,
+    CourseTicketDisplayFormPage, OnlinePaymentDisplayFormPage,
+    AccommodationManageFormPage, BedTicketDisplayFormPage,)
+from waeup.sirp.students.interfaces import (
+    IStudentsContainer, IStudent, IStudentClearance,
+    IStudentStudyCourse, IStudentAccommodation, IStudentStudyLevel,
+    ICourseTicket, IStudentOnlinePayment, IBedTicket,
+    )
+
+from waeup.sirp.interfaces import MessageFactory as _
+
+grok.context(ISIRPObject) # Make ISIRPObject 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(ISIRPObject)
+    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.getStudent(), self.link)
+        portal_language = getUtility(ISIRPUtils).PORTAL_LANGUAGE
+        lang = self.request.cookies.get('sirp.language', portal_language)
+        text = translate(self.text, 'waeup.sirp',
+            target_language=lang)
+        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.getStudent(), attr=self.link)
+        if slip:
+            url = self.view.url(self.context,self.link)
+            return u'<li><a href="%s">%s</a></li>' % (
+                    url, self.text)
+        return ''
+
+class StudentManageBaseLink(StudentManageLink):
+    grok.order(2)
+    link = 'index'
+    text = _(u'Base Data')
+
+class StudentManageClearanceLink(StudentManageLink):
+    grok.order(3)
+    link = 'view_clearance'
+    text = _(u'Clearance Data')
+
+class StudentManagePersonalLink(StudentManageLink):
+    grok.order(4)
+    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.payStudent')
+    link = 'payments'
+    text = _(u'Payments')
+
+class StudentManageAccommodationLink(StudentManageLink):
+    grok.order(7)
+    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(4)
+    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 StudentClearanceManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentClearanceDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Manage')
+    target = 'edit_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 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 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.pdf'
+
+class StudentPersonalEditActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentPersonalDisplayFormPage)
+    grok.require('waeup.viewStudent')
+    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'
+
+class CourseRegistrationSlipActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudentStudyLevel)
+    grok.view(StudyLevelDisplayFormPage)
+    grok.require('waeup.viewStudent')
+    icon = 'actionicon_pdf.png'
+    text = _('Download course registration slip')
+    target = 'course_registration.pdf'
+
+class StudyLevelManageActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IStudentStudyLevel)
+    grok.view(StudyLevelDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Manage')
+    target = 'manage'
+
+class StudentValidateCoursesActionButton(ManageActionButton):
+    grok.order(3)
+    grok.context(IStudentStudyLevel)
+    grok.view(StudyLevelDisplayFormPage)
+    grok.require('waeup.validateStudent')
+    text = _('Validate courses')
+    target = 'validate_courses'
+    icon = 'actionicon_accept.png'
+
+    @property
+    def target_url(self):
+        if self.context.getStudent().state != REGISTERED or \
+            str(self.context.__parent__.current_level) != self.context.__name__:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class StudentRejectCoursesActionButton(ManageActionButton):
+    grok.order(4)
+    grok.context(IStudentStudyLevel)
+    grok.view(StudyLevelDisplayFormPage)
+    grok.require('waeup.validateStudent')
+    text = _('Reject courses')
+    target = 'reject_courses'
+    icon = 'actionicon_reject.png'
+
+    @property
+    def target_url(self):
+        if self.context.getStudent().state not in (VALIDATED, REGISTERED) or \
+            str(self.context.__parent__.current_level) != self.context.__name__:
+            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(1)
+    grok.context(IStudentOnlinePayment)
+    grok.view(OnlinePaymentDisplayFormPage)
+    grok.require('waeup.viewStudent')
+    icon = 'actionicon_pdf.png'
+    text = _('Download payment receipt')
+    target = 'payment_receipt.pdf'
+
+    @property
+    def target_url(self):
+        if self.context.p_state != 'paid':
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class RequestCallbackActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IStudentOnlinePayment)
+    grok.view(OnlinePaymentDisplayFormPage)
+    grok.require('waeup.payStudent')
+    icon = 'actionicon_call.png'
+    text = _('Request callback')
+    target = 'callback'
+
+    @property
+    def target_url(self):
+        if self.context.p_state != 'unpaid':
+            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.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 base data')
+    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):
+        if self.context.state != ADMITTED:
+            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 CourseRegistrationStartActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudentStudyCourse)
+    grok.view(StudyCourseDisplayFormPage)
+    grok.require('waeup.handleStudent')
+    icon = 'actionicon_start.gif'
+    text = _('Start course registration')
+    target = 'start_course_registration'
+
+    @property
+    def target_url(self):
+        if not self.context.getStudent().state in (CLEARED,RETURNING):
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+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.getStudent()
+        condition1 = student.state != PAID
+        condition2 = str(student['studycourse'].current_level) in \
+            self.view.context.keys()
+        if condition1 or condition2:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class StudyLevelEditActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudentStudyLevel)
+    grok.view(StudyLevelDisplayFormPage)
+    grok.require('waeup.handleStudent')
+    text = _('Add and remove courses')
+    target = 'edit'
+
+    @property
+    def target_url(self):
+        student = self.view.context.getStudent()
+        condition1 = student.state != PAID
+        condition2 = student[
+            'studycourse'].current_level != self.view.context.level
+        if condition1 or condition2:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class StudentsTab(PrimaryNavTab):
+    """Students tab in primary navigation.
+    """
+
+    grok.context(ISIRPObject)
+    grok.order(4)
+    grok.require('waeup.viewStudentsTab')
+
+    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.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')
+    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_url = self.view.application_url() + (
+            '/students/%s' % self.request.principal.id)
+        #app_slip = getUtility(IExtFileStore).getFileByContext(
+        #    self.context.getStudent(), '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)
+    write_log_message(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
+    if upload.filename.count('.') == 0:
+        view.flash(_('File name has no extension.'))
+        return False
+    if upload.filename.count('.') > 1:
+        view.flash(_('File name contains more than one dot.'))
+        return False
+    basename, expected_ext = os.path.splitext(download_name)
+    dummy, ext = os.path.splitext(upload.filename)
+    ext.lower()
+    if expected_ext:
+        if ext != expected_ext:
+            view.flash(_('${a} file extension expected.',
+                mapping = {'a':expected_ext.replace('.','')}))
+            return False
+    else:
+        if not ext.replace('.','') in ALLOWED_FILE_EXTENSIONS:
+            view.flash(
+                _('Only the following extension are allowed: ${a}',
+                mapping = {'a':', '.join(ALLOWED_FILE_EXTENSIONS)}))
+            return False
+        download_name += ext
+    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
+    store = getUtility(IExtFileStore)
+    file_id = IFileStoreNameChooser(context).chooseName(attr=download_name)
+    store.createFile(file_id, upload)
+    write_log_message(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(IStudentClearance)
+    grok.viewletmanager(FileManager)
+    grok.view(StudentClearanceDisplayFormPage)
+    grok.template('filedisplay')
+    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(IStudentClearance)
+    grok.viewletmanager(FileManager)
+    grok.view(StudentClearanceManageFormPage)
+    grok.template('fileupload')
+    grok.require('waeup.uploadStudentFile')
+    tab_redirect = '?tab2'
+    mus = 1024 * 150
+    upload_button =_('Upload new file')
+    delete_button = _('Delete attachment')
+
+    @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 AcceptanceLetterDisplay(FileDisplay):
+    """Acceptance Letter display viewlet.
+    """
+    grok.order(1)
+    label = _(u'Acceptance Letter')
+    title = _(u'Acceptance Letter Scan')
+    download_name = u'acceptance_letter'
+
+class AcceptanceLetterSlip(AcceptanceLetterDisplay):
+    grok.view(ExportPDFClearanceSlipPage)
+
+class AcceptanceLetterUpload(FileUpload):
+    """AcceptanceLetter upload viewlet.
+    """
+    grok.order(2)
+    label = _(u'Acceptance Letter')
+    title = _(u'Acceptance Letter Scan')
+    mus = 1024 * 150
+    download_name = u'acceptance_letter'
+    tab_redirect = '?tab2'
+
+class Image(grok.View):
+    """Renders images for students.
+    """
+    grok.baseclass()
+    grok.name('none.jpg')
+    grok.context(IStudentClearance)
+    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'
+
+class AcceptanceLetterImage(Image):
+    """Renders acceptance letter scan.
+    """
+    grok.name('acceptance_letter')
+    download_name = u'acceptance_letter'
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/vocabularies.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/vocabularies.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/vocabularies.py	(revision 7760)
@@ -0,0 +1,189 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.interfaces import SimpleSIRPVocabulary
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.students.lgas import LGAS
+from waeup.sirp.students.nats import NATS
+from waeup.sirp.university.vocabularies import course_levels
+
+lgas_vocab = SimpleSIRPVocabulary(
+    *sorted([(x[1],x[0]) for x in LGAS]))
+
+nats_vocab = SimpleSIRPVocabulary(
+    *sorted([(x[1],x[0]) for x in NATS]))
+
+def study_levels(studycourse):
+    if studycourse.certificate is not None:
+        start_level = int(studycourse.certificate.start_level)
+        end_level = int(studycourse.certificate.end_level)
+        if start_level == 10:
+            levels = [10,] + [level for level in range(100,end_level+200,10)
+                if level % 100 < 30]
+        else:
+            levels = [level for level in range(start_level,end_level+200,10)
+                if level % 100 < 30]
+    else:
+        # default level range
+        levels = [level for level in range(100,800,10) if level % 100 < 30]
+    return levels
+
+
+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):
+        if context.certificate is not None:
+            start_level = int(context.certificate.start_level)
+            end_level = int(context.certificate.end_level)
+        else:
+            # default level range
+            start_level = 100
+            end_level = 600
+        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 CertificateSource(BasicContextualSourceFactory):
+    """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=('', 'z*'))),
+                    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[:64])
+
+
+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):
+    implements(ISource)
+    cat_name = 'students_catalog'
+    field_name = 'reg_number'
+    validation_error = RegNumNotInSource
+    def __init__(self, context):
+        self.context = context
+        return
+
+    def __contains__(self, value):
+        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 entry.student_id != self.context.student_id:
+                # XXX: sources should simply return False.
+                #      But then we get some stupid error message in forms
+                #      when validation fails.
+                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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/workflow.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/workflow.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/students/workflow.py	(revision 7760)
@@ -0,0 +1,206 @@
+"""Workflow for students.
+"""
+import grok
+from zope.component import getUtility
+from hurry.workflow.workflow import Transition, WorkflowState, NullCondition
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowTransitionEvent
+from waeup.sirp.interfaces import (
+    IObjectHistory, ISIRPWorkflowInfo, ISIRPUtils,
+    CREATED, ADMITTED, CLEARANCE, REQUESTED, CLEARED, PAID, RETURNING,
+    REGISTERED, VALIDATED)
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.workflow import SIRPWorkflow, SIRPWorkflowInfo
+from waeup.sirp.students.interfaces import IStudent, IStudentsUtils
+
+
+IMPORTABLE_STATES = (ADMITTED, CLEARANCE, REQUESTED, CLEARED, PAID, RETURNING,
+    REGISTERED, VALIDATED)
+
+REGISTRATION_TRANSITIONS = (
+    Transition(
+        transition_id = 'create',
+        title = _('Create student'),
+        source = None,
+        condition = NullCondition,
+        msg = _('Student record created'),
+        destination = CREATED),
+
+    Transition(
+        transition_id = 'admit',
+        title = _('Admit student'),
+        msg = _('Student admitted'),
+        source = CREATED,
+        destination = ADMITTED),
+
+    Transition(
+        transition_id = 'reset1',
+        title = _('Reset student'),
+        msg = _('Student record reset'),
+        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 = _("Student record 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'),
+        msg = _("Student record reset to 'clearance'"),
+        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'),
+        msg = _("Student record reset to 'clearance'"),
+        source = CLEARED,
+        destination = CLEARANCE),
+
+    Transition(
+        transition_id = 'pay_first_school_fee',
+        title = _('Pay school fee'),
+        msg = _('School fee paid'),
+        source = CLEARED,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'reset5',
+        title = _('Reset to cleared'),
+        msg = _("Student record reset to 'cleared'"),
+        source = PAID,
+        destination = CLEARED),
+
+    Transition(
+        transition_id = 'pay_school_fee',
+        title = _('Pay school fee'),
+        msg = _('School fee paid'),
+        source = RETURNING,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'reset6',
+        title = _('Reset to returning'),
+        msg = _("Student record 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 paid'),
+        msg = _("Student record reset to 'paid'"),
+        source = REGISTERED,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'validate_courses',
+        title = _('Validate courses'),
+        msg = _('Courses validated'),
+        source = REGISTERED,
+        destination = VALIDATED),
+
+    Transition(
+        transition_id = 'reset8',
+        title = _('Reset to paid'),
+        msg = _("Student record reset to 'paid'"),
+        source = VALIDATED,
+        destination = PAID),
+
+    Transition(
+        transition_id = 'return',
+        title = _('Return'),
+        msg = _('Returned'),
+        source = VALIDATED,
+        destination = RETURNING),
+
+    Transition(
+        transition_id = 'reset9',
+        title = _('Reset to validated'),
+        msg = _("Student record reset to 'validated'"),
+        source = RETURNING,
+        destination = VALIDATED),
+    )
+
+LOCK_CLEARANCE_TRANS = ('reset2', 'request_clearance')
+UNLOCK_CLEARANCE_TRANS = ('reset3', 'reset4', 'start_clearance')
+
+registration_workflow = SIRPWorkflow(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(SIRPWorkflowInfo, grok.Adapter):
+    """Adapter to adapt Student objects to workflow info objects.
+    """
+    grok.context(IStudent)
+    grok.provides(ISIRPWorkflowInfo)
+
+    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.
+    """
+
+    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
+    # Student data don't change after first-time payment
+    if event.transition.transition_id == 'pay_first_school_fee':
+        pass
+    # School fee payment of returning students triggers the change of
+    # current session, current level, and current verdict
+    if event.transition.transition_id == 'pay_school_fee':
+        getUtility(IStudentsUtils).setReturningData(obj)
+    # 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/testing.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/testing.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/testing.py	(revision 7760)
@@ -0,0 +1,429 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp`.
+"""
+import grok
+import doctest
+import logging
+import os.path
+import re
+import tempfile
+import shutil
+import unittest
+import warnings
+import zope.component
+import waeup.sirp
+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.sirp.__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.sirp' in x]
+    if ws_loggers:
+        # For debugging: show any remaining loggers from w.s. 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 new_loggers:
+        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.sirp` package.
+
+    For many tests, even simple ones, we want the components defined
+    somewhere in the :mod:`waeup.sirp` 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.sirp` 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.sirp.testing import (
+        maybe_grok, setUpZope, cleanUpZope,
+        )
+      from waeup.sirp.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.sirp`
+    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.sirp.testing.SIRPUnitTestLayer`.
+
+    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.sirp.testing.SIRPUnitTestLayer`
+
+    """
+    gsm =  getGlobalSiteManager()
+    # If there are any event handlers registered already, we assume
+    # that waeup.sirp 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.sirp')
+    warnings.simplefilter('default') # reenable warnings
+    return True
+
+def setup_datacenter_conf():
+    """Register a datacenter config utility for non-functional tests.
+    """
+    from waeup.sirp.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.sirp.interfaces import IDataCenterConfig
+    conf = queryUtility(IDataCenterConfig)
+    if conf is None:
+        return
+    path = conf['path']
+    shutil.rmtree(path)
+    gsm = getGlobalSiteManager()
+    gsm.unregisterUtility(conf)
+    return
+
+class SIRPUnitTestLayer(object):
+    """A layer for tests that groks `waeup.sirp`.
+
+    A Zope test layer that registers all :mod:`waeup.sirp` components
+    before attached tests are run and cleans this registrations up
+    afterwards. Also basic (non-waeup.sirp) 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.sirp.testing import SIRPUnitTestLayer
+
+      class MyTestCase(unittest.TestCase):
+
+          layer = SIRPUnitTestLayer
+
+          # 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):
+        cleanUpZope(None)
+        teardown_datacenter_conf()
+        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.
+#:
+#: `<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]{10}'), '<10-DIGITS>'),
+    (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.sirp.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.sirp.testing`.
+
+        If optional `optionflags` are not given, use ``REPORT_NDIFF``,
+        ``ELLIPSIS``, and ``NORMALIZE_WHITESPACE``.
+
+        .. seealso:: :data:`waeup.sirp.testing.optionflags`
+
+        .. seealso:: :data:`waeup.sirp.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.s. 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.sirp.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
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/__init__.py	(revision 7760)
@@ -0,0 +1,2 @@
+# This is a package
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_app.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_app.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_app.py	(revision 7760)
@@ -0,0 +1,63 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.app import University
+from waeup.sirp.interfaces import IUniversity
+from waeup.sirp.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())
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_authentication.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_authentication.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_authentication.py	(revision 7760)
@@ -0,0 +1,88 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 unittest
+from zope.component.hooks import setSite, clearSite
+from zope.interface.verify import verifyClass, verifyObject
+from zope.pluggableauth.interfaces import IAuthenticatorPlugin
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from waeup.sirp.testing import FunctionalTestCase, FunctionalLayer
+from waeup.sirp.authentication import (
+    UserAuthenticatorPlugin, Account, SIRPPrincipalInfo,
+    get_principal_role_manager)
+
+class FakeSite(grok.Site, grok.Container):
+    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(self.site)
+        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, SIRPPrincipalInfo))
+        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, SIRPPrincipalInfo))
+        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
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_configuration.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_configuration.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_configuration.py	(revision 7760)
@@ -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.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.configuration import (
+    ConfigurationContainer, SessionConfiguration, SessionConfigurationFactory)
+from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_doctests.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_doctests.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_doctests.py	(revision 7760)
@@ -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.sirp.
+"""
+from waeup.sirp.testing import get_doctest_suite
+
+def test_suite():
+    suite = get_doctest_suite([
+            'authentication.txt',
+            ])
+    return suite
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_imagestorage.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_imagestorage.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_imagestorage.py	(revision 7760)
@@ -0,0 +1,404 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.app import University
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.imagestorage import (
+    FileStoreNameChooser, ExtFileStore, DefaultFileStoreHandler,
+    DefaultStorage)
+from waeup.sirp.interfaces import (
+    IFileStoreNameChooser, IExtFileStore, IFileStoreHandler,)
+
+class HelperFuncsTests(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+
+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)
+        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):
+        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.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)
+        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()
+        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()
+        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()
+        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()
+        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()
+        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
+        fs = ExtFileStore()
+        context = CustomContext()
+        file_id1 = IFileStoreNameChooser(context).chooseName()
+        file_id2 = IFileStoreNameChooser(context).chooseName(attr='img')
+        file_id3 = IFileStoreNameChooser(context).chooseName(attr='doc')
+        fs = ExtFileStore()
+        # 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 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()
+        # 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()
+        context = CustomContext()
+        file_id1 = IFileStoreNameChooser(context).chooseName()
+        file_id2 = IFileStoreNameChooser(context).chooseName(attr='img')
+        file_id3 = IFileStoreNameChooser(context).chooseName(attr='doc')
+        fs = ExtFileStore()
+        # 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_interfaces.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_interfaces.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_interfaces.py	(revision 7760)
@@ -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
+##
+
+# Tests for interfaces and included components.
+import grok
+import unittest
+from waeup.sirp.interfaces import DuplicationError, RoleSource
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+
+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 NonSIRPRole(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('NonSIRPRole', NonSIRPRole)
+        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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_objecthistory.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_objecthistory.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_objecthistory.py	(revision 7760)
@@ -0,0 +1,100 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""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.sirp.app import University
+from waeup.sirp.interfaces import IObjectHistory, ISIRPObject
+from waeup.sirp.testing import FunctionalTestCase, FunctionalLayer
+from waeup.sirp.objecthistory import ObjectHistory
+
+class SampleObject(grok.Model):
+    grok.implements(ISIRPObject)
+    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> - 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> - blah by Bob', 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_smtp.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_smtp.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_smtp.py	(revision 7760)
@@ -0,0 +1,354 @@
+# -*- 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.sirp.app import University
+from waeup.sirp.interfaces import IMailService
+from waeup.sirp.smtp import (
+    encode_header_item, encode_address, encode_body, FakeSMTPDelivery,
+    send_mail)
+from waeup.sirp.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-user@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')
+        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?=')
+        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:',
+             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: 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:',
+             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: 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:',
+             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: 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 SIRP',
+            '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 SIRP',
+            '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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_waeup.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_waeup.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/test_waeup.py	(revision 7760)
@@ -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.sirp package.
+"""
+import grok
+import re
+import zope.component.eventtesting
+from zope.testing import renormalizing
+from waeup.sirp.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.sirp package
+test_suite = grok.testing.register_all_tests(
+    'waeup.sirp', checker=checker, usetup=setUpZope,
+    layer=FunctionalLayer)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/util.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/util.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/tests/util.py	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/__init__.py	(revision 7760)
@@ -0,0 +1,12 @@
+# Make this a package.
+from waeup.sirp.university.course import Course
+from waeup.sirp.university.coursescontainer import CoursesContainer
+from waeup.sirp.university.certificate import Certificate
+from waeup.sirp.university.certificatescontainer import CertificatesContainer
+from waeup.sirp.university.faculty import Faculty
+from waeup.sirp.university.department import Department
+from waeup.sirp.university.facultiescontainer import FacultiesContainer
+__all__ = (
+    'Course', 'CoursesContainer',
+    'Certificate', 'CertificatesContainer',
+    'Faculty', 'FacultiesContainer', 'Department')
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/api.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/api.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/api.txt	(revision 7760)
@@ -0,0 +1,30 @@
+:mod:`waeup.sirp.university` -- academic components
+***************************************************
+
+.. module:: waeup.sirp.university
+
+Components that represent academic 'core' stuff like faculties, 
+
+Submodules
+==========
+
+The :mod:`waeup.sirp.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
+
+   courserefs.txt
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/batching.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/batching.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/batching.py	(revision 7760)
@@ -0,0 +1,280 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 waeup.sirp.interfaces import IBatchProcessor
+from waeup.sirp.university.interfaces import (
+    IFacultiesContainer, IFaculty, ICourse, IDepartment, ICertificate,
+    ICertificateCourse)
+from waeup.sirp.utils.batching import BatchProcessor
+
+class FacultyProcessor(BatchProcessor):
+    """A batch processor for IFaculty objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'facultyimporter'
+    grok.name(util_name)
+
+    name = u'Faculty Importer'
+    iface = IFaculty
+
+    location_fields = ['code',]
+    factory_name = 'waeup.Faculty'
+
+    mode = None
+
+    def parentsExist(self, row, site):
+        return 'faculties' in site.keys()
+
+    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
+
+class DepartmentProcessor(BatchProcessor):
+    """A batch processor for IDepartment objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'departmentimporter'
+    grok.name(util_name)
+
+    name = u'Department Importer'
+    iface = IDepartment
+
+    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 CourseProcessor(BatchProcessor):
+    """A batch processor for ICourse objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'courseimporter'
+    grok.name(util_name)
+
+    name = u'Course Importer'
+    iface = ICourse
+
+    location_fields = ['code', 'faculty_code', 'department_code']
+    factory_name = 'waeup.Course'
+
+    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']]
+        return row['department_code'] in faculty.keys()
+
+    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):
+        dept = site['faculties'][row['faculty_code']][row['department_code']]
+        return dept.courses
+
+    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.addCourse(obj)
+        return
+
+    def delEntry(self, row, site):
+        parent = self.getParent(row, site)
+        del parent[row['code']]
+        return
+
+class CertificateProcessor(BatchProcessor):
+    """A batch processor for ICertificate objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'certificateimporter'
+    grok.name(util_name)
+
+    name = u'Certificate Importer'
+    iface = ICertificate
+
+    location_fields = ['code', 'faculty_code', 'department_code']
+    factory_name = 'waeup.Certificate'
+
+    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']]
+        return row['department_code'] in faculty.keys()
+
+    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):
+        dept = site['faculties'][row['faculty_code']][row['department_code']]
+        return dept.certificates
+
+    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.addCertificate(obj)
+        return
+
+    def delEntry(self, row, site):
+        parent = self.getParent(row, site)
+        del parent[row['code']]
+        return
+
+class CertificateCourseProcessor(BatchProcessor):
+    """A batch processor for ICertificateCourse objects.
+    """
+    grok.implements(IBatchProcessor)
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    util_name = 'certificatecourseimporter'
+    grok.name(util_name)
+
+    name = u'CertificateCourse Importer'
+    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.addCourseRef(row['course'],
+                            row['level'], row['mandatory'])
+        return
+
+    def delEntry(self, row, site):
+        parent = self.getParent(row, site)
+        parent.delCourseRef(row['course'].code, row['level'])
+        return
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/catalog.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/catalog.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/catalog.py	(revision 7760)
@@ -0,0 +1,193 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 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.sirp.catalog import QueryResultItem
+from waeup.sirp.interfaces import IUniversity, IQueryResultItem
+from waeup.sirp.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 = grok.index.Field(attribute='code')
+    title = grok.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 = grok.index.Field(attribute='code')
+    application_category = grok.index.Field(attribute='application_category')
+    title = grok.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 = grok.index.Field(attribute='getCourseCode')
+    level = grok.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.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.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.type = 'Course Referrer'
+
+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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/certificate.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/certificate.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/certificate.py	(revision 7760)
@@ -0,0 +1,148 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""SIRP certificates
+"""
+import grok
+from zope.event import notify
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility
+from zope.component.interfaces import IFactory, ComponentLookupError
+from zope.interface import implementedBy
+from waeup.sirp.university.interfaces import (
+    ICertificate, ICertificateAdd, ICertificateCourse)
+from waeup.sirp.university.vocabularies import course_levels
+
+class Certificate(grok.Container):
+    """A certificate.
+    """
+    grok.implements(ICertificate, ICertificateAdd)
+
+    @property       # Make this method read_only and looking like an attr.
+    def local_roles(self):
+        return ['waeup.local.CourseAdviser100',
+                'waeup.local.CourseAdviser200',
+                'waeup.local.CourseAdviser300',
+                'waeup.local.CourseAdviser400',
+                'waeup.local.CourseAdviser500',
+                'waeup.local.CourseAdviser600',
+                ]
+
+    def __init__(self, code=u'NA', title=u'Unnamed Certificate',
+                 study_mode=None, start_level=None,
+                 end_level=None, application_category=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
+
+    def longtitle(self):
+        return "%s (%s)" % (self.title,self.code)
+
+    def addCourseRef(self, course, level=100, mandatory=True):
+        """Add a course referrer.
+        """
+        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 delCourseRef(self, code, level=None):
+        """Delete a course referrer denoted by its code.
+        """
+        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
+
+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
+
+    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.
+    """
+    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.loggerInfo('ObjectRemovedEvent', 'removed: certificate')
+    return
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/certificate.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/certificate.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/certificate.txt	(revision 7760)
@@ -0,0 +1,366 @@
+:mod:`waeup.sirp.university.certificate` -- Certificates for SIRP
+******************************************************************
+
+.. module:: waeup.sirp.university.certificate
+
+Components that represent and manage certificates.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.SIRPUnitTestLayer
+
+
+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.sirp.university.certificate import Certificate
+     >>> mycertificate = Certificate()
+
+   Certificates have the attributes required by the `ICertificate` interface:
+
+     >>> from waeup.sirp.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:: addCourseRef(course[, level=100,[ mandatory=True]])
+
+      Add a referrer of a course. A course is an object implementing
+      :class:`waeup.sirp.interfaces.ICourse`.
+
+      Please don't be confused by the term 'referrer'. As course objects 
+      will normaly be  stored
+      in a department, the course referrer refers to 
+      the 'real' course in the department container.
+
+   .. method:: delCourseRef(code)
+
+      Remove a course referrer from a certificate.
+
+      The referrer must be given by its code number.
+
+:class:`CertificateCourse`
+--------------------------
+
+.. class:: CertificateCourse(course[, level=100[, mandatory=True]])
+
+   Create a course referrer.
+
+   A certificate course (old term) or course referrer are held in
+   certificates and refer to an existing :class:`Course` instance held
+   elsewhere.
+
+   A certificate can require several course referrers and one
+   course referrer 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.sirp.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.sirp.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.sirp.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.sirp.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.sirp.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.sirp.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 course referrer 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.sirp.university.certificate import Certificate
+    >>> mycertificate = Certificate()
+    >>> mycertificate
+    <waeup.sirp.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.sirp.university.certificate.Certificate object at 0x...>
+
+CertificateCourses
+------------------
+
+:class:`CertificateCourse` instances comply with the
+:class:`ICertificateCourse` interface:
+
+    >>> from waeup.sirp.university.interfaces import ICertificateCourse
+    >>> from waeup.sirp.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.sirp.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.addCourseRef(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 course referrer 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/certificatescontainer.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/certificatescontainer.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/certificatescontainer.py	(revision 7760)
@@ -0,0 +1,131 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.interfaces import DuplicationError
+from waeup.sirp.university.interfaces import (
+    ICertificatesContainer, ICertificate)
+
+class CertificatesContainer(grok.Container):
+    """A storage for certificates.
+
+    A :class:`CertificatesContainer` stores
+    :class:`waeup.sirp.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)
+    grok.require('waeup.manageUniversity')
+
+    def __setitem__(self, name, certificate):
+        """Insert `certificate` with `name` as key into container.
+
+        The `certificate` must be an object implementing
+        :class:`waeup.sirp.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.sirp.app.University` instance), then a
+        :exc:`waeup.sirp.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.sirp.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.sirp.app.University` instance), then a
+        :exc:`waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/certificatescontainer.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/certificatescontainer.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/certificatescontainer.txt	(revision 7760)
@@ -0,0 +1,149 @@
+:mod:`waeup.sirp.university.certificatescontainer` -- Certificate containers
+****************************************************************************
+
+.. module:: waeup.sirp.university.certificatescontainer
+
+Containers for certificates.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.SIRPUnitTestLayer
+
+
+Content Classes (models and containers)
+=======================================
+
+:class:`CertificatesContainer`
+------------------------------
+
+.. class:: CertificatesContainer()
+
+   Create a certificate container instance.
+
+   .. attribute:: grok.implements(ICertificatesContainer)
+
+   .. attribute:: grok.require('waeup.manageUniversity')
+
+   :class:`CertificatesContainer` instances are instances of
+   :class:`grok.Container` that hold and manage instances of
+   :class:`waeup.sirp.university.certificate.Certificate`.
+
+     >>> from waeup.sirp.university.certificatescontainer import CertificatesContainer
+     >>> mycontainer = CertificatesContainer()
+     >>> mycontainer
+     <waeup.sirp.university.certificatescontainer.CertificatesContainer object at 0x...>
+
+   Certificate containers provide `ICertificatesContainer`:
+
+     >>> from waeup.sirp.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.sirp.university.certificate.Certificate`:
+
+        >>> from waeup.sirp.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.sirp.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.sirp.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.sirp.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.sirp.university.certificatescontainer.CertificatesContainer>
+
+
+Examples
+========
+
+Getting a certificate container
+-------------------------------
+
+We can easily create `CertificatesContainers`:
+
+    >>> from waeup.sirp.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.sirp.university.certificatescontainer.CertificatesContainer object at 0x...>
+
+This way we get a thing that implements ICertificatesContainer without
+imports or similar.
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/course.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/course.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/course.py	(revision 7760)
@@ -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
+##
+"""Courses.
+"""
+import grok
+from zope.catalog.interfaces import ICatalog
+from zope.interface import implementedBy
+from zope.component import getUtility
+from zope.component.interfaces import IFactory, ComponentLookupError
+from waeup.sirp.university.interfaces import ICourse, ICourseAdd
+
+class Course(grok.Model):
+    """A university course.
+    """
+    grok.implements(ICourse, ICourseAdd)
+
+    def __init__(self,
+                 title=u'Unnamed Course',
+                 code=u'NA',
+                 credits=0,
+                 passmark=40,
+                 semester=1, **kw):
+        super(Course, self).__init__(**kw)
+        self.title = title
+        self.code = code
+        self.credits = credits
+        self.passmark = passmark
+        self.semester = semester
+
+    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))
+    for certcourse in results:
+        # Remove that referrer...
+        cert = certcourse.__parent__
+        cert.delCourseRef(code)
+        cert._p_changed = True
+    return
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/course.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/course.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/course.txt	(revision 7760)
@@ -0,0 +1,143 @@
+:mod:`waeup.sirp.university.course` -- Courses
+**********************************************
+
+.. module:: waeup.sirp.university.course
+
+Components that represent courses.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.SIRPUnitTestLayer
+
+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.sirp.university.course import Course
+     >>> mycourse = Course()
+     >>> mycourse
+     <waeup.sirp.university.course.Course object at 0x...>
+
+   Course instances have the attributes required by the
+   :class:`waeup.sirp.interfaces.ICourse` interface:
+
+     >>> from waeup.sirp.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.sirp.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.sirp.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.sirp.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.sirp.university.course import Course
+    >>> mycourse = Course()
+    >>> mycourse
+    <waeup.sirp.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.sirp.university.course.Course object at 0x...>
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/courserefs.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/courserefs.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/courserefs.txt	(revision 7760)
@@ -0,0 +1,116 @@
+Course referrers
+****************
+
+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.sirp.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.sirp.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'].addCourseRef(
+    ...   fac['DEPT'].courses['CRS1'])
+
+    >>> fac['DEPT'].certificates['CERT'].addCourseRef(
+    ...   fac['DEPT2'].courses['CRS2'])
+
+We add the latter course again, but this time for the second level:
+
+    >>> fac['DEPT'].certificates['CERT'].addCourseRef(
+    ...   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']
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/coursescontainer.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/coursescontainer.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/coursescontainer.py	(revision 7760)
@@ -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
+##
+"""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.sirp.interfaces import DuplicationError
+from waeup.sirp.university.interfaces import ICoursesContainer, ICourse
+
+class CoursesContainer(grok.Container):
+    """See interfaces for description.
+    """
+    grok.implements(ICoursesContainer)
+    grok.require('waeup.manageUniversity')
+
+    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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/coursescontainer.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/coursescontainer.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/coursescontainer.txt	(revision 7760)
@@ -0,0 +1,147 @@
+:mod:`waeup.sirp.university.coursescontainer` -- Course containers
+******************************************************************
+
+.. module:: waeup.sirp.university.coursescontainer
+
+Containers for courses.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.SIRPUnitTestLayer
+
+
+Content Classes (models and containers)
+=======================================
+
+:class:`CoursesContainer`
+-------------------------
+
+.. class:: CoursesContainer()
+
+   Create a course container instance.
+
+   .. attribute:: grok.implements(ICoursesContainer)
+
+   .. attribute:: grok.require('waeup.manageUniversity')
+
+   :class:`CoursesContainer` instances are instances of
+   :class:`grok.Container` that hold and manage instances of
+   :class:`waeup.sirp.university.course.Course`.
+
+     >>> from waeup.sirp.university.coursescontainer import CoursesContainer
+     >>> mycontainer = CoursesContainer()
+     >>> mycontainer
+     <waeup.sirp.university.coursescontainer.CoursesContainer object at 0x...>
+
+   Course containers provide `ICoursesContainer`:
+
+     >>> from waeup.sirp.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.sirp.university.course.Course`:
+
+        >>> from waeup.sirp.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.sirp.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.sirp.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.sirp.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.sirp.university.coursescontainer.CoursesContainer>
+
+
+Examples
+========
+
+Getting a course container
+--------------------------
+
+We can easily create `CoursesContainers`:
+
+    >>> from waeup.sirp.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.sirp.university.coursescontainer.CoursesContainer object at 0x...>
+
+This way we get a thing that implements ICoursesContainer without
+imports or similar.
+
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/department.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/department.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/department.py	(revision 7760)
@@ -0,0 +1,95 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.component import getUtility
+from waeup.sirp.university.coursescontainer import CoursesContainer
+from waeup.sirp.university.certificatescontainer import CertificatesContainer
+from waeup.sirp.interfaces import ISIRPUtils
+from waeup.sirp.university.interfaces import IDepartment, IDepartmentAdd
+
+class Department(grok.Container):
+    """A university department.
+    """
+    grok.implements(IDepartment, IDepartmentAdd)
+
+    @property       # Make this method read_only and looking like an attr.
+    def local_roles(self):
+        return ['waeup.local.DepartmentManager',
+                'waeup.local.ClearanceOfficer',
+                'waeup.local.CourseAdviser100',
+                'waeup.local.CourseAdviser200',
+                'waeup.local.CourseAdviser300',
+                'waeup.local.CourseAdviser400',
+                'waeup.local.CourseAdviser500',
+                'waeup.local.CourseAdviser600',
+                ]
+
+    # A simple counter for ids.
+    next_id = 0L
+
+    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'
+
+    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
+        return None
+
+    def longtitle(self):
+        insttypes_dict = getUtility(ISIRPUtils).getInstTypeDict()
+        return "%s %s (%s)" % (
+            insttypes_dict[self.title_prefix],
+            self.title, self.code)
+
+
+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)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/department.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/department.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/department.txt	(revision 7760)
@@ -0,0 +1,135 @@
+:mod:`waeup.sirp.university.department` -- Departments
+******************************************************
+
+.. module:: waeup.sirp.university.department
+
+Components that represent university departments.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.SIRPUnitTestLayer
+
+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.sirp.university.department import Department
+     >>> mydept = Department()
+     >>> mydept
+     <waeup.sirp.university.department.Department object at 0x...>
+
+   Another way to create :class:`Department` instances is by asking
+   for a factory called ``waeup.sirp.Department``. This way we can create a
+   department without importing a class:
+
+     >>> from zope.component import createObject
+     >>> mydept = createObject(u'waeup.Department')
+     >>> mydept
+     <waeup.sirp.university.department.Department object at 0x...>
+
+   :class:`Department` instances have the attributes required by the
+   `IDepartment` interface:
+
+     >>> from waeup.sirp.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.sirp.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.sirp...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.sirp.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.sirp.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.sirp.university.department.Department>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/export.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/export.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/export.py	(revision 7760)
@@ -0,0 +1,246 @@
+## $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 csv
+import grok
+from cStringIO import StringIO
+from waeup.sirp.interfaces import ICSVExporter
+
+class FacultyExporter(grok.GlobalUtility):
+    """Exporter for faculties.
+    """
+    grok.implements(ICSVExporter)
+    grok.name('faculties')
+
+    #: Fieldnames considered by this exporter
+    fields = ('code', 'title', 'title_prefix')
+
+    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 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.
+        """
+        if filepath is None:
+            outfile = StringIO()
+        else:
+            outfile = open(filepath, 'wb')
+        writer = csv.DictWriter(outfile, self.fields)
+        writer.writerow(dict(zip(self.fields, self.fields))) # header
+        return writer, outfile
+
+    def write_item(self, faculty, writer):
+        """Write a row into using `writer`.
+        """
+        row = {}
+        for name in self.fields:
+            value = getattr(faculty, name, None)
+            value = self.mangle_value(value, name, faculty)
+            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 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.
+        """
+        writer, outfile = self.get_csv_writer(filepath)
+        faculties = site.get('faculties', {})
+        return self.export(faculties.values(), filepath)
+        for faculty in faculties.values():
+            self.write_item(faculty, writer)
+        return self.close_outfile(filepath, outfile)
+
+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')
+
+    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 faculties in facultycontainer 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')
+
+    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__.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 faculties in facultycontainer 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')
+
+    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.
+        """
+        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')
+
+    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 faculties in facultycontainer 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/facultiescontainer.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/facultiescontainer.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/facultiescontainer.py	(revision 7760)
@@ -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
+##
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.sirp.interfaces import ISIRPPluggable
+from waeup.sirp.university.interfaces import IFacultiesContainer, IFaculty
+
+class FacultiesContainer(grok.Container):
+    """See interfaces for description.
+    """
+    grok.implements(IFacultiesContainer)
+    grok.require('waeup.manageUniversity')
+
+    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(ISIRPPluggable)
+    grok.name('faculties')
+
+    def setup(self, site, name, logger):
+        if 'faculties' in site.keys():
+            logger.warn('Could not create container for faculties in SIRP.')
+            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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/facultiescontainer.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/facultiescontainer.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/facultiescontainer.txt	(revision 7760)
@@ -0,0 +1,178 @@
+:mod:`waeup.sirp.university.facultiescontainer` -- Faculty Containers
+*********************************************************************
+
+.. module:: waeup.sirp.university.facultiescontainer
+
+Components that represent faculty containers.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.SIRPUnitTestLayer
+
+
+Content Classes (models and containers)
+=======================================
+
+:class:`FacultiesContainer`
+---------------------------
+
+.. class:: FacultiesContainer()
+
+   Create a faculty container:
+
+     >>> from waeup.sirp.university.facultiescontainer import FacultiesContainer
+     >>> mycontainer = FacultiesContainer()
+     >>> mycontainer
+     <waeup.sirp.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.sirp.university.facultiescontainer.FacultiesContainer object at 0x...>
+
+   :class:`FacultiesContainer` instances have the attributes required by the
+   `IFacultiesContainer` interface:
+
+     >>> from waeup.sirp.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.sirp.university.faculty import Faculty
+        >>> mycontainer.addFaculty(Faculty(title='Physics',
+        ...                                code='FP'))
+
+        >>> list(mycontainer.items())
+        [(u'FP', <waeup.sirp.university.faculty.Faculty object at 0x...>)]
+
+
+
+Utilities
+=========
+
+:class:`AcademicsPlugin`
+------------------------
+
+.. class:: AcademicsPlugin()
+
+   .. attribute:: grok.implements(ISIRPPluggable)
+
+   This plugin component tells under which name (``faculties``) an
+   instance of academics stuff should be created in plain SIRP
+   instances. It also tells the factory name for FacultiesContainer
+   instances, which serve as root object for academics stuff in SIRP 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.sirp.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.sirp.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.sirp.university.facultiescontainer.FacultiesContainer>
+
+Examples
+========
+
+We can easily create `FacultiesContainers`:
+
+    >>> from waeup.sirp.university.facultiescontainer import FacultiesContainer
+    >>> mycontainer = FacultiesContainer()
+
+Faculty containers provide `IFacultiesContainer`:
+
+    >>> from waeup.sirp.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.sirp.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.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/faculty.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/faculty.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/faculty.py	(revision 7760)
@@ -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
+##
+"""Faculty components.
+"""
+
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from zope.component import getUtility
+from waeup.sirp.interfaces import ISIRPUtils
+from waeup.sirp.university.interfaces import (
+    IFaculty, IFacultyAdd, IDepartment)
+
+class Faculty(grok.Container):
+    """A university faculty.
+    """
+    grok.implements(IFaculty, IFacultyAdd)
+
+    @property       # Make this method read_only and looking like an attr.
+    def local_roles(self):
+        return ['waeup.local.DepartmentManager',
+                'waeup.local.ClearanceOfficer',
+                'waeup.local.CourseAdviser100',
+                'waeup.local.CourseAdviser200',
+                'waeup.local.CourseAdviser300',
+                'waeup.local.CourseAdviser400',
+                'waeup.local.CourseAdviser500',
+                'waeup.local.CourseAdviser600',
+                ]
+
+    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
+
+    def longtitle(self):
+        insttypes_dict = getUtility(ISIRPUtils).getInstTypeDict()
+        result = "%s %s (%s)" % (
+            insttypes_dict[self.title_prefix],
+            self.title, self.code)
+        return result
+
+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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/faculty.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/faculty.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/faculty.txt	(revision 7760)
@@ -0,0 +1,200 @@
+:mod:`waeup.sirp.university.faculty` -- Faculties
+*************************************************
+
+.. module:: waeup.sirp.university.faculty
+
+Components that represent university faculties.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.SIRPUnitTestLayer
+
+
+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.sirp.university.faculty import Faculty
+     >>> myfac = Faculty()
+     >>> myfac
+     <waeup.sirp.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.sirp.university.faculty.Faculty object at 0x...>
+
+   :class:`Faculty` instances have the attributes required by the
+   `IFaculty` interface:
+
+     >>> from waeup.sirp.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.sirp.university.department import Department
+        >>> myfac.addDepartment(Department(title='Physics',
+        ...                                code='DP'))
+
+        >>> list(myfac.items())
+        [(u'DP', <waeup.sirp.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.sirp.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.sirp.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.sirp.university.faculty.Faculty>
+
+
+Examples:
+=========
+
+We can create faculties:
+
+    >>> from waeup.sirp.university.faculty import Faculty
+    >>> myfaculty = Faculty()
+    >>> myfaculty
+    <waeup.sirp.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.sirp.university.faculty.Faculty object at 0x...>
+
+Faculty attributes
+==================
+
+Faculties have the attributes required by the `IFaculty` interface:
+
+    >>> from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/interfaces.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/interfaces.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/interfaces.py	(revision 7760)
@@ -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
+##
+"""Interfaces of academics specific objects.
+"""
+
+from zope import schema
+from zope.interface import Attribute
+from waeup.sirp.interfaces import (ISIRPObject, ISIRPContainer)
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.university.vocabularies import (
+    course_levels,
+    CourseSource,
+    StudyModeSource,
+    AppCatSource,
+    InstTypeSource,
+    SemesterSource,
+    )
+
+class IFaculty(ISIRPContainer):
+    """Representation of a university faculty.
+    """
+    code = schema.TextLine(
+        title = _(u'Code'),
+        default = u'NA',
+        required = True,
+        readonly = 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,
+        )
+
+    def longtitle():
+        """
+        Returns the long title of a faculty.
+        """
+
+class IFacultyAdd(IFaculty):
+    """Representation of a university faculty.
+    """
+    code = schema.TextLine(
+        title = _(u'Code'),
+        description = _(u'This code will become part of the URL.'),
+        default = u'NA',
+        required = True,
+        readonly = False,
+        )
+
+IFacultyAdd['code'].order =  IFaculty['code'].order
+
+class IFacultiesContainer(ISIRPContainer):
+    """A container for faculties.
+    """
+    def addFaculty(faculty):
+        """Add an IFactulty object.
+
+        """
+class IDepartment(ISIRPObject):
+    """Representation of a department.
+    """
+    code = schema.TextLine(
+        title = _(u'Code'),
+        default = u'NA',
+        required = True,
+        readonly = 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,
+        )
+
+    courses = Attribute("A container for courses.")
+    certificates = Attribute("A container for certificates.")
+
+    def longtitle():
+        """
+        Returns the long title of a department.
+        """
+
+class IDepartmentAdd(IDepartment):
+    """Representation of a university department.
+    """
+    code = schema.TextLine(
+        title = _(u'Code'),
+        description = _(u'This code will become part of the URL.'),
+        default = u'NA',
+        required = True,
+        readonly = False,
+        )
+
+IDepartmentAdd['code'].order =  IDepartment['code'].order
+
+class ICoursesContainer(ISIRPContainer):
+    """A container for faculties.
+    """
+    def addCourse(course):
+        """Add an ICourse object.
+
+        Returns the key, under which the object was stored.
+        """
+
+class ICourse(ISIRPObject):
+    """Representation of a course.
+    """
+    code = schema.TextLine(
+        title = _(u'Code'),
+        default = u'NA',
+        required = True,
+        readonly = True,
+        )
+
+    title = schema.TextLine(
+        title = _(u'Title of course'),
+        default = _(u'Unnamed'),
+        required = True,
+        )
+
+    credits = schema.Int(
+        title = _(u'Credits'),
+        default = 0,
+        required = False,
+        )
+
+    passmark = schema.Int(
+        title = _(u'Passmark'),
+        default = 40,
+        required = False,
+        )
+
+    semester = schema.Choice(
+        title = _(u'Semester/Term'),
+        default = 9,
+        source = SemesterSource(),
+        required = True,
+        )
+
+    def longtitle():
+        """
+        Returns the long title of a course.
+        """
+
+class ICourseAdd(ICourse):
+    """Representation of a course.
+    """
+    code = schema.TextLine(
+        title = _(u'Code'),
+        description = _(u'Enter unique course code which will become part of the URL.'),
+        default = u'NA',
+        required = True,
+        readonly = False,
+        )
+
+ICourseAdd['code'].order =  ICourse['code'].order
+
+class ICertificate(ISIRPObject):
+    """Representation of a certificate.
+    """
+    code = schema.TextLine(
+        title = _(u'Code'),
+        default = u'NA',
+        required = True,
+        readonly = True,
+        )
+
+    title = schema.TextLine(
+        title = _(u'Title'),
+        default = _(u'Unnamed'),
+        required = True,
+        )
+
+    study_mode = schema.Choice(
+        title = _(u'Study Mode'),
+        source = StudyModeSource(),
+        default = u'ug_ft',
+        required = True,
+        )
+
+    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,
+        )
+
+    def longtitle():
+        """
+        Returns the long title of a certificate.
+        """
+
+class ICertificateAdd(ICertificate):
+    """Representation of a certificate.
+    """
+    code = schema.TextLine(
+        title = _(u'Code'),
+        default = u'NA',
+        description = _(u'Enter unique certificate code which will become part of the URL.'),
+        required = True,
+        readonly = False,
+        )
+
+ICertificateAdd['code'].order =  ICertificate['code'].order
+
+class ICertificatesContainer(ISIRPContainer):
+    """A container for certificates.
+    """
+    def addCertificate(certificate):
+        """Add an ICertificate object.
+
+        Returns the key, under which the object was stored.
+        """
+
+class ICertificateCourse(ISIRPObject):
+    """A certificatecourse is referring a course and provides some own
+       attributes.
+    """
+    course = schema.Choice(
+        title = _(u'Course Referrer'),
+        source = CourseSource(),
+        readonly = True,
+        )
+
+    level = schema.Choice(
+        title = _(u'Level'),
+        required = True,
+        vocabulary = course_levels,
+        readonly = False,
+        )
+
+    mandatory = schema.Bool(
+        title = _(u'Is mandatory course (not elective)'),
+        required = True,
+        default = True,
+        )
+
+    def getCourseCode():
+        """Return the code of the course referred to.
+
+        This is needed for cataloging.
+        """
+
+    def longtitle():
+        """
+        Returns the long title of a certificatecourse.
+        """
+
+
+class ICertificateCourseAdd(ICertificateCourse):
+    """A certificatecourse is referring a course and
+       provides some own attributes.
+    """
+    course = schema.Choice(
+        title = _(u'Course'),
+        source = CourseSource(),
+        readonly = False,
+        )
+
+ICertificateCourseAdd['course'].order =  ICertificateCourse['course'].order
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/__init__.py	(revision 7760)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_batching.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_batching.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_batching.py	(revision 7760)
@@ -0,0 +1,328 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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
+
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.sirp.interfaces import IBatchProcessor
+from waeup.sirp.testing import FunctionalTestCase, FunctionalLayer
+from waeup.sirp.university.batching import (
+    FacultyProcessor, DepartmentProcessor, CourseProcessor,
+    CertificateProcessor, CertificateCourseProcessor)
+from waeup.sirp.university.certificate import Certificate, CertificateCourse
+from waeup.sirp.university.course import Course
+from waeup.sirp.university.department import Department
+
+
+class TestFacultyProcessor(unittest.TestCase):
+
+    def setUp(self):
+        self.proc = FacultyProcessor()
+        self.site1 = dict(faculties=dict())
+        self.site2 = dict(faculties=dict(FAC='pseudo faculty'))
+        self.row = dict(code='FAC')
+        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
+
+class TestDepartmentProcessors(unittest.TestCase):
+
+    def setUp(self):
+        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')
+        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
+
+class CourseProcessorTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CourseProcessorTests, self).setUp()
+        self.proc = CourseProcessor()
+        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.course = Course(code='CRS')
+        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', code="CRS")
+        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
+        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 True)
+        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.course)
+        return
+
+    def test_del_entry(self):
+        # make sure we can delete entries
+        self.assertTrue('CRS' in self.department2.courses.keys())
+        self.proc.delEntry(self.row, self.site4)
+        self.assertTrue('CRS' not in self.department2.courses.keys())
+        return
+
+class CertificateProcessorTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CertificateProcessorTests, self).setUp()
+        self.proc = CertificateProcessor()
+        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.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', code="CRT")
+        return
+
+    def test_ifaces(self):
+        # Make sure we fullfill all interface contracts
+        verifyClass(IBatchProcessor, CertificateProcessor)
+        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 True)
+        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.certificate)
+        return
+
+    def test_del_entry(self):
+        # make sure we can delete entries
+        self.assertTrue('CRT' in self.department2.certificates.keys())
+        self.proc.delEntry(self.row, self.site4)
+        self.assertTrue('CRT' not in self.department2.certificates.keys())
+        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'].addCourseRef(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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_certificatescontainer.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_certificatescontainer.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_certificatescontainer.py	(revision 7760)
@@ -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.sirp.app import University
+from waeup.sirp.interfaces import DuplicationError
+from waeup.sirp.testing import (
+    FunctionalLayer, doctestsuite_for_module, FunctionalTestCase)
+from waeup.sirp.university.certificate import Certificate
+from waeup.sirp.university.certificatescontainer import CertificatesContainer
+from waeup.sirp.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.sirp.university.certificatescontainer'),
+        ))
+    return suite
+
+if __name__=='__main__':
+    unittest.main(defaultTest='test_suite')
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_coursescontainer.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_coursescontainer.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_coursescontainer.py	(revision 7760)
@@ -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.sirp.university.interfaces import ICoursesContainer
+from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_export.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_export.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_export.py	(revision 7760)
@@ -0,0 +1,521 @@
+## $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 waeup.sirp.interfaces import ICSVExporter
+from waeup.sirp.testing import SIRPUnitTestLayer
+from waeup.sirp.university import (
+    FacultiesContainer, Faculty, Department, Course, Certificate,
+    )
+from waeup.sirp.university.export import (
+    FacultyExporter, DepartmentExporter, CourseExporter,
+    CertificateExporter, CertificateCourseExporter,
+    )
+
+class FacultyExporterTest(unittest.TestCase):
+
+    layer = SIRPUnitTestLayer
+
+    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\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\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\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\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 = SIRPUnitTestLayer
+
+    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)
+        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\r\n'
+            'D1,F1,Department of Cheddar,department\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\r\n'
+            'D1,F1,Department of Cheddar,department\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\r\n'
+            'D1,F1,Department of Cheddar,department\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\r\n'
+            'D1,F1,Department of Cheddar,department\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 = SIRPUnitTestLayer
+
+    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\r\n'
+            'C1,F1,D1,Cheese Basics,0,40,1\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\r\n'
+            'C1,F1,D1,Cheese Basics,0,40,1\r\n'
+            'C2,F1,D1,Advanced Cheese Making,0,40,1\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\r\n'
+            'C1,F1,D1,Cheese Basics,0,40,1\r\n'
+            'C2,F1,D1,Advanced Cheese Making,0,40,1\r\n'
+            'C3,F1,D2,Selling Cheese,0,40,1\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\r\n'
+            'C1,F1,D1,Cheese Basics,0,40,1\r\n'
+            'C2,F1,D1,Advanced Cheese Making,0,40,1\r\n'
+            'C3,F1,D2,Selling Cheese,0,40,1\r\n'
+            )
+        return
+
+class CertificateExporterTest(unittest.TestCase):
+    # Tests for CertificateExporter
+
+    layer = SIRPUnitTestLayer
+
+    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)
+        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\r\n'
+            'CERT1,F1,D1,Master of Cheese,ct_ft,100,300,basic\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\r\n'
+            'CERT1,F1,D1,Master of Cheese,ct_ft,100,300,basic\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\r\n'
+            'CERT1,F1,D1,Master of Cheese,ct_ft,100,300,basic\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\r\n'
+            'CERT1,F1,D1,Master of Cheese,ct_ft,100,300,basic\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 = SIRPUnitTestLayer
+
+    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.addCourseRef(self.course1, 100, True)
+        self.cert1.addCourseRef(self.course2, 400, False)
+        self.cert3.addCourseRef(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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_facultiescontainer.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_facultiescontainer.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_facultiescontainer.py	(revision 7760)
@@ -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.sirp.testing import FunctionalTestCase, FunctionalLayer
+from waeup.sirp.university.interfaces import IFacultiesContainer
+from waeup.sirp.university import FacultiesContainer
+from waeup.sirp.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 SIRP.\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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_faculty.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_faculty.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_faculty.py	(revision 7760)
@@ -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.sirp.university import Faculty, Department
+from waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_university.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_university.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/tests/test_university.py	(revision 7760)
@@ -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.sirp.university import *
+from waeup.sirp.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/courserefs.txt',])
+    # add local unittests (actually only one)
+    for testcase in [UniversitySubpackageTests,]:
+        suite.addTests(unittest.TestLoader().loadTestsFromTestCase(
+                testcase))
+    return suite
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/vocabularies.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/vocabularies.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/university/vocabularies.py	(revision 7760)
@@ -0,0 +1,112 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 zc.sourcefactory.contextual import BasicContextualSourceFactory
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility
+from waeup.sirp.interfaces import SimpleSIRPVocabulary, ISIRPUtils
+from waeup.sirp.interfaces import MessageFactory as _
+
+course_levels = SimpleSIRPVocabulary(
+    (_('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),
+    )
+
+class SemesterSource(BasicContextualSourceFactory):
+    """An institution type source delivers semester or term descriptors.
+    """
+    def getValues(self, context):
+        semesters_dict = getUtility(ISIRPUtils).getSemesterDict()
+        return semesters_dict.keys()
+
+    def getToken(self, context, value):
+        return str(value)
+
+    def getTitle(self, context, value):
+        semesters_dict = getUtility(ISIRPUtils).getSemesterDict()
+        return semesters_dict[value]
+
+class InstTypeSource(BasicContextualSourceFactory):
+    """An institution type source delivers types of institutions
+    in the portal.
+    """
+    def getValues(self, context):
+        insttypes_dict = getUtility(ISIRPUtils).getInstTypeDict()
+        return sorted(insttypes_dict.keys())
+
+    def getToken(self, context, value):
+        return value
+
+    def getTitle(self, context, value):
+        insttypes_dict = getUtility(ISIRPUtils).getInstTypeDict()
+        return insttypes_dict[value]
+
+class AppCatSource(BasicContextualSourceFactory):
+    """A application category source delivers all application categories
+    provided in the portal.
+    """
+    def getValues(self, context):
+        appcats_dict = getUtility(ISIRPUtils).getAppCatDict()
+        return sorted(appcats_dict.keys())
+
+    def getToken(self, context, value):
+        return value
+
+    def getTitle(self, context, value):
+        appcats_dict = getUtility(ISIRPUtils).getAppCatDict()
+        return appcats_dict[value]
+
+class StudyModeSource(BasicContextualSourceFactory):
+    """A study modes source delivers all study modes provided
+    in the portal.
+    """
+    def getValues(self, context):
+        studymodes_dict = getUtility(ISIRPUtils).getStudyModesDict()
+        return sorted(studymodes_dict.keys())
+
+    def getToken(self, context, value):
+        return value
+
+    def getTitle(self, context, value):
+        studymodes_dict = getUtility(ISIRPUtils).getStudyModesDict()
+        return studymodes_dict[value]
+
+class CourseSource(BasicSourceFactory):
+    """A course source delivers all courses inside the portal by looking
+       up a catalog.
+    """
+    def getValues(self):
+        catalog = getUtility(ICatalog, name='courses_catalog')
+        return sorted(list(
+                catalog.searchResults(
+                    code=('', 'z*'))),key=lambda value: value.code)
+
+    def getToken(self, value):
+        return value.code
+
+    def getTitle(self, value):
+        return "%s - %s" % (value.code, value.title[:64])
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/userscontainer.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/userscontainer.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/userscontainer.py	(revision 7760)
@@ -0,0 +1,60 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Users container for the SIRP portal.
+"""
+import grok
+from zope.event import notify
+from waeup.sirp.authentication import Account
+from waeup.sirp.interfaces import IUsersContainer
+from waeup.sirp.utils.logger import Logger
+
+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, 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,
+                             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)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/userscontainer.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/userscontainer.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/userscontainer.txt	(revision 7760)
@@ -0,0 +1,65 @@
+User container for the SIRP
+***************************
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.SIRPUnitTestLayer
+
+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.sirp.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.sirp.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
+    '...15aca8166'
+
+Remark: that we can tell the last chars of the 'encrypted' password,
+means, that the encryption is broken or at least waeker as it should
+be.
+
+XXX: We could provide a stronger (correct) SHA encryption.
+
+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)
+    []
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/api.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/api.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/api.txt	(revision 7760)
@@ -0,0 +1,19 @@
+:mod:`waeup.sirp.utils` -- misc. helpers
+****************************************
+
+.. module:: waeup.sirp.utils
+
+Components and other stuff of general interest.
+
+Submodules
+==========
+
+The :mod:`waeup.sirp.utils` module provides the following submodules:
+
+.. toctree::
+   :maxdepth: 1
+
+   helpers.txt
+   batching.txt
+   importexport.txt
+   converters.txt
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/batching.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/batching.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/batching.py	(revision 7760)
@@ -0,0 +1,344 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""SIRP 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 copy
+import csv
+import os
+import sys
+import tempfile
+import time
+from zope.component import createObject
+from zope.interface import Interface
+from zope.schema import getFields
+from waeup.sirp.interfaces import (
+    IBatchProcessor, FatalCSVError, DuplicationError, IObjectConverter)
+
+class BatchProcessor(grok.GlobalUtility):
+    """A processor to add, update, or remove data.
+
+    This is a non-active baseclass.
+    """
+    grok.provides(IBatchProcessor)
+    grok.context(Interface)
+    grok.baseclass()
+
+    # Name used in pages and forms...
+    name = u'Non-registered base importer'
+
+    # Internal name...
+    util_name = 'baseimporter'
+
+    # 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):
+        result = []
+        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 = csv.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 updateEntry(self, obj, row, site):
+        """Update obj to the values given in row.
+        """
+        for key, value in row.items():
+            # Skip fields not declared in interface.
+            if hasattr(obj, key):
+                setattr(obj, key, value)
+        return
+
+    def createLogfile(self, path, fail_path, num, warnings, mode, user,
+                      timedelta, logger=None):
+        """Write to log file.
+        """
+        if logger is None:
+            return
+        status = 'OK'
+        if warnings > 0:
+            status = 'FAILED'
+        logger.info("-" * 20)
+        logger.info("%s: Batch processing finished: %s" % (user, status))
+        logger.info("%s: Source: %s" % (user, path))
+        logger.info("%s: Mode: %s" % (user, mode))
+        logger.info("%s: User: %s" % (user, user))
+        if warnings > 0:
+            logger.info("%s: Failed datasets: %s" % (
+                    user, os.path.basename(fail_path)))
+        logger.info("%s: Processing time: %0.3f s (%0.4f s/item)" % (
+                user, timedelta, timedelta/(num or 1)))
+        logger.info("%s: Processed: %s lines (%s successful/ %s failed)" % (
+                user, num, num - warnings, warnings
+                ))
+        logger.info("-" * 20)
+        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'):
+        """Validates all values in row.
+        """
+        converter = IObjectConverter(self.iface)
+        errs, inv_errs, conv_dict =  converter.fromStringDict(
+            row, self.factory_name)
+        return errs, inv_errs, conv_dict
+
+    def doImport(self, path, headerfields, mode='create', user='Unknown',
+                 logger=None):
+        """Perform actual import.
+        """
+        time_start = time.time()
+        self.checkHeaders(headerfields, mode)
+        mapping = self.getMapping(path, headerfields, mode)
+        reader = csv.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 = csv.DictWriter(open(failed_path, 'wb'),
+                                       failed_headers)
+        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 = csv.DictWriter(open(finished_path, 'wb'),
+                                         finished_headers)
+        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)
+            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 in the same container. 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)
+                try:
+                    self.addEntry(obj, row, site)
+                except KeyError, error:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "%s Skipping." % error.message)
+                    continue
+            elif mode == 'remove':
+                if not self.entryExists(row, site):
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, string_row,
+                        "Cannot remove: no such entry.")
+                    continue
+                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
+                self.updateEntry(obj, row, site)
+            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)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/batching.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/batching.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/batching.txt	(revision 7760)
@@ -0,0 +1,569 @@
+:mod:`waeup.sirp.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 importers or processors, that are part of
+the batch-processing mechanism.
+
+Importers/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.sirp.utils.batching import BatchProcessor
+    >>> 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):
+    ...     # This is not strictly necessary, as the default
+    ...     # updateEntry method does exactly the same
+    ...     for key, value in row.items():
+    ...       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()
+    --------------------
+    Bob: Batch processing finished: OK
+    Bob: Source: newcomers.csv
+    Bob: Mode: create
+    Bob: User: Bob
+    Bob: Processing time: ... s (... s/item)
+    Bob: Processed: 4 lines (4 successful/ 0 failed)
+    --------------------
+
+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()
+    --------------------
+    ...
+    --------------------
+    Bob: Batch processing finished: FAILED
+    Bob: Source: newcomers.csv
+    Bob: Mode: create
+    Bob: User: Bob
+    Bob: Failed datasets: newcomers.pending.csv
+    Bob: Processing time: ... s (... s/item)
+    Bob: Processed: 4 lines (0 successful/ 4 failed)
+    --------------------
+
+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 in the same container. Skipping.
+    Wilma,Wilmas Asylum,1,1,This object already exists in the same container. Skipping.
+    Fred,Freds Dinoburgers,0,10,This object already exists in the same container. Skipping.
+    Joey,Joeys Drive-in,0,110,This object already exists in the same container. 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 in the same container. Skipping.
+    Wilmas Asylum,1,This object already exists in the same container. Skipping.
+    Freds Dinoburgers,10,This object already exists in the same container. Skipping.
+    Joeys Drive-in,110,This object already exists in the same container. 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!
+
+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",,"Wilma"
+    ... """)
+
+    >>> result = processor.doImport('newcomers.csv', ['name',
+    ...                             'dinoports', 'owner'],
+    ...                    mode='update', user='Bob')
+    >>> result
+    (1, 0, '...', None)
+
+    >>> wilma.dinoports is None
+    True
+
+Clean up:
+
+    >>> shutil.rmtree(os.path.dirname(result[2]))
+
+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 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/converters.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/converters.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/converters.py	(revision 7760)
@@ -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
+##
+"""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, ISimpleInputWidget
+from zope.interface import Interface
+from zope.publisher.browser import TestRequest
+from waeup.sirp.interfaces import IObjectConverter
+
+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 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.provides(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):
+        """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.
+        """
+        if form_fields is None:
+            form_fields = self.default_form_fields
+
+        request = TestRequest(form={})
+        for key, val in data_dict.items():
+            request.form['form.%s' % key] = val
+
+        obj = context
+        if isinstance(context, basestring):
+            obj = createObject(context)
+
+        widgets = form.setUpInputWidgets(
+            form_fields, 'form', obj, request)
+
+        new_data = dict()
+        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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/converters.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/converters.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/converters.txt	(revision 7760)
@@ -0,0 +1,861 @@
+:mod:`waeup.sirp.utils.converters` -- Converters
+************************************************
+
+.. module:: waeup.sirp.utils.converters
+
+Converters for :mod:`zope.schema` based data.
+
+.. contents::
+
+
+.. :NOdoctest:
+.. :NOlayer: waeup.sirp.testing.SIRPUnitTestLayer
+
+
+Constants
+=========
+
+.. attribute:: NONE_STRING_VALUE
+
+   This value determines, which string represents
+   'non-values'. Current setting is:
+
+      >>> from waeup.sirp.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.sirp.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.sirp.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.sirp.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.sirp.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.sirp.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.sirp.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.sirp.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.sirp.utils.converters import TextConverter
+    >>> converter = TextConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.sirp.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.sirp.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.sirp.utils.converters import BoolConverter
+    >>> converter = BoolConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.sirp.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.sirp.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.sirp.utils.converters import IntConverter
+    >>> converter = IntConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.sirp.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.sirp.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.sirp.utils.converters import ChoiceConverter
+    >>> converter = ChoiceConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.sirp.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.sirp.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.sirp.interfaces import SimpleSIRPVocabulary
+    >>> field = Choice(
+    ...   title = u'Favourite Dish',
+    ...   default = 0,
+    ...   vocabulary = SimpleSIRPVocabulary(
+    ...        ('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.sirp.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.sirp.utils.converters import DateConverter
+    >>> converter = DateConverter(field)
+
+Or we can just grab a registered adapter:
+
+    >>> from waeup.sirp.interfaces import ISchemaTypeConverter
+    >>> converter = ISchemaTypeConverter(field)
+
+This will select the correct converter for us automatically.
+
+    >>> converter
+    <waeup.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/helpers.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/helpers.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/helpers.py	(revision 7760)
@@ -0,0 +1,479 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 SIRP.
+"""
+import os
+import re
+import sys
+import shutil
+import grok
+from cStringIO import StringIO
+from docutils.core import publish_string
+from zope.component import getUtility, getUtilitiesFor
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+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.sirp` we use factories extensively for
+    batching. While processing a batch some importer 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.sirp.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.sirp.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.sirp.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.sirp`
+    system.
+
+       >>> grok.testing.grok('waeup.sirp.utils.helpers')
+       >>> grok.testing.grok_component(
+       ...    'MyObjectFactory', MyObjectFactory
+       ...  )
+       True
+
+    After grokking we (and importers) 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.sirp.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.sirp.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.sirp.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):
+    """Turn the attributes of a class into FieldProperty instances.
+
+    With Python >= 2.6 we can even use this function as a class decorator.
+    """
+    iface = list(implementedBy(cls))[0]
+    for field_name in getFieldNames(iface):
+        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
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/helpers.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/helpers.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/helpers.txt	(revision 7760)
@@ -0,0 +1,228 @@
+:mod:`waeup.sirp.utils.helpers` -- Helpers for SIRP
+***************************************************
+
+.. module:: waeup.sirp.utils.helpers
+
+Helper functions for SIRP.
+
+.. :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.sirp.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.sirp.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.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/importexport.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/importexport.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/importexport.py	(revision 7760)
@@ -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 csv
+import grok
+import cPickle
+import zope.xmlpickle
+from cStringIO import StringIO
+from zope.interface import Interface
+from waeup.sirp.interfaces import (ISIRPObject, ISIRPExporter,
+                                   ISIRPXMLExporter, ISIRPXMLImporter)
+
+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 SIRP object as pickle.
+
+    This all-purpose exporter exports attributes defined in schemata
+    and contained objects (if the exported thing is a container).
+    """
+    grok.context(ISIRPObject)
+    grok.provides(ISIRPExporter)
+
+    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 SIRP object as XML.
+
+    This all-purpose exporter exports XML representations of pickable
+    objects.
+    """
+    grok.context(Interface)
+    grok.provides(ISIRPXMLExporter)
+
+    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 SIRP object from XML.
+    """
+    grok.context(Interface)
+    grok.provides(ISIRPXMLImporter)
+
+    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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/importexport.txt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/importexport.txt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/importexport.txt	(revision 7760)
@@ -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.sirp.testing.SIRPUnitTestLayer
+
+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.sirp.interfaces import ISIRPXMLExporter
+    >>> exporter = ISIRPXMLExporter(mycave)
+    >>> exporter
+    <waeup.sirp.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.sirp.interfaces import ISIRPXMLImporter
+    >>> importer = ISIRPXMLImporter(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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/logger.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/logger.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/logger.py	(revision 7760)
@@ -0,0 +1,390 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.utils.logging` is a mix-in class
+:class:`waeup.sirp.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.sirp.${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.sirp.utils.logger import Logger
+
+  class MyComponent(object, Logger):
+      # Yes that's a complete working class
+      logger_name = 'waeup.sirp.${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.sirp.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 string import Template
+from zope import schema
+from zope.component import queryUtility
+from zope.interface import Interface, Attribute, implements
+from waeup.sirp.interfaces import (
+    IDataCenter, IDataCenterStorageMovedEvent, ILoggerCollector)
+from waeup.sirp.utils.helpers import get_current_principal
+
+#: Default logfile size (5 KB)
+MAX_BYTES = 5 * 1024 ** 2
+
+#: Default num of backup files (5)
+BACKUP_COUNT = 5
+
+#: 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.sirp.${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.sirp.${sitename}'
+    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)
+        flt = ContextFilter()
+        logger.addFilter(flt)
+        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 = logging.handlers.RotatingFileHandler(
+            filename, maxBytes=MAX_BYTES, backupCount=BACKUP_COUNT)
+        handler.setLevel(LEVEL)
+        formatter = logging.Formatter(
+            '%(asctime)s - %(levelname)s - %(user)s - %(message)s')
+        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)
+        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.sirp.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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/overrides.zcml
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/overrides.zcml	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/overrides.zcml	(revision 7760)
@@ -0,0 +1,19 @@
+<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"
+      />
+
+  <adapter
+      for="zope.schema.interfaces.IDate
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory="waeup.sirp.widgets.datewidget.FormattedDateWidget"
+      permission="zope.Public"
+      />
+
+</configure>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_batching.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_batching.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_batching.py	(revision 7760)
@@ -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
+##
+import doctest
+import logging
+import os
+import shutil
+import tempfile
+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.interface import Interface, implements
+from waeup.sirp.app import University
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+
+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
+#Cave = attrs_to_fields(Cave)
+
+stoneville = dict
+
+from waeup.sirp.utils.batching import BatchProcessor
+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):
+        # This is not strictly necessary, as the default
+        # updateEntry method does exactly the same
+        for key, value in row.items():
+            setattr(obj, key, value)
+
+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(
+            """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
+""")
+        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_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
+           --------------------
+           Bob: Batch processing finished: OK
+           Bob: Source: /.../newcomers.csv
+           Bob: Mode: create
+           Bob: User: Bob
+           Bob: Processing time: ... s (... s/item)
+           Bob: Processed: 4 lines (4 successful/ 0 failed)
+           --------------------
+
+        """
+        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
+
+#def test_suite():
+#    return unittest.TestSuite()
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_converters.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_converters.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_converters.py	(revision 7760)
@@ -0,0 +1,331 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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
+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)
+
+from waeup.sirp.app import University
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.university import Faculty
+from waeup.sirp.utils.converters import IObjectConverter
+from waeup.sirp.utils.helpers import attrs_to_fields
+from waeup.sirp.interfaces import SimpleSIRPVocabulary
+
+colors = SimpleSIRPVocabulary(
+    ('Red', u'red'),
+    ('Green', u'green'),
+    ('Blue', u'blue'),
+    )
+car_nums = SimpleSIRPVocabulary(
+    ('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,
+        )
+    @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 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_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
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_doctests.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_doctests.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_doctests.py	(revision 7760)
@@ -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.sirp.testing import get_doctest_suite
+
+def test_suite():
+    suite = get_doctest_suite(['utils/batching.txt'])
+    return suite
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_helpers.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_helpers.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_helpers.py	(revision 7760)
@@ -0,0 +1,266 @@
+# -*- 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 os
+import shutil
+import tempfile
+import unittest
+import doctest
+from cStringIO import StringIO
+from zope.security.testing import Principal, Participation
+from zope.security.management import newInteraction, endInteraction
+from waeup.sirp.utils import helpers
+
+from zope.interface import Interface, implements
+class IFakeObject(Interface):
+    """Some marker interface."""
+
+class FakeObject(object):
+    implements(IFakeObject)
+
+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')
+        f1 = open(p1, 'wb').write('Hi!')
+        f2 = 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')
+        f1 = open(p1, 'wb').write('Hi!')
+        f2 = 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)
+
+def test_suite():
+    suite = unittest.TestSuite()
+    # Register local test cases...
+    for testcase in [
+        ReST2HTMLTestCase,
+        FactoryBaseTestCase,
+        CopyFileSystemTreeTestCase,
+        RemoveFileOrDirectoryTestCase,
+        CurrentPrincipalTestCase,
+        CmpFilesTestCase,
+        FileSizeTestCase,
+        ]:
+        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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_logger.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_logger.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/tests/test_logger.py	(revision 7760)
@@ -0,0 +1,421 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.sirp.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.sirp.app import University
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+
+from waeup.sirp.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]
+        self.assertEqual(handler.maxBytes, MAX_BYTES)
+        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)
+        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.RotatingFileHandler))
+
+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.RotatingFileHandler))
+        self.assertEqual(handler.maxBytes, MAX_BYTES)
+        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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/utils.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/utils.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/utils/utils.py	(revision 7760)
@@ -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
+##
+"""General helper utilities for SIRP.
+"""
+import os
+import grok
+import string
+from zope.i18n import translate
+from random import SystemRandom as r
+from waeup.sirp.interfaces import ISIRPUtils
+from waeup.sirp.interfaces import MessageFactory as _
+from waeup.sirp.smtp import send_mail as send_mail_internally
+
+def send_mail(from_name,from_addr,rcpt_name,rcpt_addr,subject,body,config):
+    """Wrapper for the real SMTP functionality in :mod:`waeup.sirp.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
+
+class SIRPUtils(grok.GlobalUtility):
+    """A collection of parameters and methods subject to customization.
+    """
+    grok.implements(ISIRPUtils)
+    # 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'),
+        }
+
+    def getInstTypeDict(self):
+        """Provide a dictionary of study modes.
+        """
+        return {
+        'faculty': 'Faculty of',
+        'department': 'Department of',
+        'school': 'School of',
+        'office': 'Office for',
+        'centre': 'Centre for',
+        'institute': 'Institute of',
+        'school_for': 'School for',
+        }
+
+    def getStudyModesDict(self):
+        """Provide a dictionary of study modes.
+        """
+        return {
+        'rmd_ft': 'Remedial with deficiencies',
+        'dp_pt': 'Diploma Part Time',
+        'ct_ft': 'Certificate Full Time',
+        'dp_ft': 'Diploma Full Time',
+        'de_pt': 'Direct Entry Part Time',
+        'pg_ft': 'Postgraduate Full Time',
+        'pg_pt': 'Postgraduate Part Time',
+        'jm_ft': 'Joint Matriculation Full Time',
+        'ume_ft': 'UME Full Time',
+        'de_ft': 'Direct Entry Full Time',
+        'ph_ft': 'Post Higher Education Full Time',
+        'transfer_pt': 'Transfer Part Time',
+        'ug_pt': 'Undergraduate Part Time',
+        'transfer_ft': 'Transfer Full Time',
+        'ct_pt': 'Certificate Part Time',
+        'ug_ft': 'Undergraduate Full Time',
+        'rm_ft': 'Remedial'
+        }
+
+    def getAppCatDict(self):
+        """Provide a dictionary of study modes.
+        """
+        return {
+        'basic': 'PUME, PDE, PCE, PRENCE',
+        'no': 'no application',
+        'pg': 'Postgraduate',
+        'sandwich': 'Sandwich',
+        'cest': 'Part-Time, Diploma, Certificate'
+        }
+
+    def getSemesterDict(self):
+        """Provide a dictionary of semester or trimester types.
+        """
+        return {
+        1: 'First Semester',
+        2: 'Second Semester',
+        3: 'Combined',
+        9: 'N/A'
+        }
+
+    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.sirp',
+            target_language=self.PORTAL_LANGUAGE)
+        return send_mail(
+            from_name,from_addr,rcpt_name,rcpt_addr,subject,body,config)
+
+    def fullname(self,firstname,lastname,middlename=None):
+        """Full name constructor.
+        """
+        # We do not necessarily have the middlename attribute
+        if middlename:
+            return string.capwords(
+                '%s %s %s' % (firstname, middlename, lastname))
+        else:
+            return string.capwords(
+                '%s %s' % (firstname, lastname))
+
+    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, login_url=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 SIRP credentials'
+        text = _(u"""Dear ${a},
+
+${b}
+Student Registration and Information Portal of
+${c}.
+
+Your user name: ${d}
+Your password: ${e}
+Login page: ${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':login_url})
+
+        body = translate(text, 'waeup.sirp',
+            target_language=self.PORTAL_LANGUAGE)
+        return send_mail(
+            from_name,from_addr,rcpt_name,rcpt_addr,subject,body,config)
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/__init__.py	(revision 7760)
@@ -0,0 +1,2 @@
+"""JS-driven widgets for SIRP.
+"""
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/datewidget.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/datewidget.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/datewidget.py	(revision 7760)
@@ -0,0 +1,165 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.
+"""
+from datetime import datetime
+from zope.formlib.i18n import _
+from zope.formlib.interfaces import ConversionError, IDisplayWidget
+from zope.formlib.textwidgets import (
+    DateWidget, DateDisplayWidget, escape, DatetimeDisplayWidget)
+from zope.formlib.widget import renderElement, CustomWidgetFactory
+from zope.interface import implements
+
+class FormattedDateWidget(DateWidget):
+    """A date widget that supports different (and _explicit_) date formats.
+
+    This is an input widget.
+    """
+    date_format = '%Y-%m-%d'
+
+    def _toFieldValue(self, input):
+        if input == self._missing:
+            return self.context.missing_value
+        else:
+            try:
+                # 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('#') 
+                value = datetime.strptime(input, self.date_format)
+            except (ValueError, IndexError), v:
+                raise ConversionError("Invalid datetime data", v)
+        return value.date()
+
+    def _toFormValue(self, value):
+        if value:
+            value = value.strftime(self.date_format)
+        return value
+
+class FormattedDateDisplayWidget(DateDisplayWidget):
+    """A date widget that supports different (and _explicit_) date formats.
+
+    This is a display widget.
+    """
+    date_format = '%Y-%m-%d'
+    implements(IDisplayWidget)
+    
+    def __call__(self):
+        if self._renderedValueSet():
+            content = self._data
+        else:
+            content = self.context.default
+        if content == self.context.missing_value:
+            return ""
+        content = content.strftime(self.date_format)
+        return renderElement("span", contents=escape(content),
+                             cssClass=self.cssClass)
+
+#: 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'),
+    }
+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 nor 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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/interfaces.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/interfaces.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/interfaces.py	(revision 7760)
@@ -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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/objectwidget.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/objectwidget.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/objectwidget.pt	(revision 7760)
@@ -0,0 +1,5 @@
+  <td class="row" tal:repeat="widget context/subwidgets">
+    <span tal:define="error widget/error"
+	  tal:replace="structure error" tal:condition="error" />
+    <span tal:replace="structure widget" />
+  </td>
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/objectwidget.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/objectwidget.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/objectwidget.py	(revision 7760)
@@ -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
+##
+"""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 SIRPObjectWidgetView(ObjectWidgetView):
+    template = ViewPageTemplateFile('objectwidget.pt')
+
+class SIRPObjectWidget(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 SIRPObjectWidgetView(self, request)
+
+class SIRPObjectDisplayWidget(SIRPObjectWidget):
+
+    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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/phonewidget.pt
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/phonewidget.pt	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/phonewidget.pt	(revision 7760)
@@ -0,0 +1,3 @@
++&nbsp;<span tal:replace="structure view/widgets/country" />&nbsp;-
+<span tal:replace="structure view/widgets/area" />&nbsp;-
+<span tal:replace="structure view/widgets/extension" />
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/phonewidget.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/phonewidget.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/phonewidget.py	(revision 7760)
@@ -0,0 +1,247 @@
+"""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).
+
+The code was mainly taken from usphone implementation in z3c.widget
+(which was designed for outdated zope.app.form (instead of
+zope.formlib or newer form libs). Therefore some modifications were
+neccessary and it might look a bit overloaded.
+
+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. All three fields require pure numbers as
+input and do not accept other chars.
+
+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.
+
+Large parts of this module are copied from z3c.widget and may be
+covered by ZPL.
+
+"""
+import re
+import copy
+from waeup.sirp.interfaces import MessageFactory as _
+from zope import schema
+from zope.browserpage import ViewPageTemplateFile
+from zope.component import getMultiAdapter
+from zope.formlib import form
+from zope.formlib.interfaces import (
+    IBrowserWidget, IWidgetInputErrorView, IInputWidget, WidgetInputError,
+    MissingInputError)
+from zope.formlib.widget import SimpleInputWidget
+from zope.interface import Interface, implements
+from zope.schema.interfaces import RequiredMissing
+
+class IPhoneData(Interface):
+    """A schema used to generate a Phone widget."""
+
+    country = schema.TextLine(
+        title=u'Country Code',
+        description=u'The country code of the phone number.',
+        min_length=1,
+        constraint=re.compile(r'^[0-9]+$').search,
+        default=u'234', # int. code for nigeria
+        required=True)
+
+    area = schema.TextLine(
+        title=u'Area Code',
+        description=u'The area code of the phone number.',
+        min_length=1,
+        constraint=re.compile(r'^[0-9]+$').search,
+        required=True)
+
+    extension = schema.TextLine(
+        title=u'Direct Line',
+        description=u'The direct line of the phone number.',
+        min_length=3,
+        constraint=re.compile(r'^[0-9]{3,}$').search,
+        required=True)
+
+class PhoneWidgetData(object):
+    """Phone number data"""
+    implements(IPhoneData)
+
+    country = None
+    area = None
+    extension = None
+
+    def __init__(self, context, number=None):
+        """Set countrycode, areacode, and extension line from number.
+        """
+        self.context = context
+        if number is None:
+            return
+        # handle other types than strings.
+        if not isinstance(number, basestring):
+            number = str(number)
+        parts = number.split('-', 2)
+        if len(parts) == 2:
+            parts = [None,] + parts
+        elif len(parts) == 0:
+            return
+        elif len(parts) == 1:
+            parts = [None, None] + parts
+        self.country, self.area, self.extension = parts
+        return
+
+    @property
+    def number(self):
+        """Return a valid phone number string of format ``<IC>-<AC>-<EL>``.
+
+        where <IC> is the country code (digits only), <AC> is the area
+        code and <EL> is the extension line.
+        """
+        return u'-'.join(
+            (self.country or '', self.area or '', self.extension))
+
+
+class PhoneWidget(SimpleInputWidget):
+    """Phone Number Widget"""
+    implements(IBrowserWidget, IInputWidget)
+
+    template = ViewPageTemplateFile('phonewidget.pt')
+    _prefix = 'field.'
+    _error = None
+    widgets = {}
+
+    # See zope.formlib.interfaces.IWidget
+    name = None
+    visible = True
+
+    def __init__(self, field, request):
+        super(PhoneWidget, self).__init__(field, request)
+        value = field.query(field.context)
+        self._generateSubWidgets()
+        return
+
+    def _generateSubWidgets(self):
+        """Create the three subwidgets.
+        """
+        value = self.context.query(self.context.context)
+        # We have to modify the adapter according to our own
+        # 'required' state. Some ugly workaround
+        adapter = copy.deepcopy(IPhoneData)
+        for name in 'country', 'area', 'extension':
+            adapter[name].required = self.required
+        adapters = {adapter: PhoneWidgetData(self, value)}
+        self.widgets = form.setUpEditWidgets(
+            form.FormFields(IPhoneData),
+            self.name, value, self.request, adapters=adapters)
+        # The displayWidth cant'be set
+        # because it is overridden by Bootstrap css classes
+        #self.widgets['country'].displayWidth = 3
+        #self.widgets['area'].displayWidth = 5
+        #self.widgets['extension'].displayWidth = 10
+        # We have to set the cssClass attributes
+        self.widgets['country'].cssClass = 'span2'
+        self.widgets['area'].cssClass = 'span2'
+        self.widgets['extension'].cssClass = 'span3'
+
+        return
+
+    def setRenderedValue(self, value):
+        """See zope.formlib.interfaces.IWidget"""
+        if isinstance(value, unicode) and '-' in value:
+            country, area, extension = value.split('-', 2)
+            self.widgets['country'].setRenderedValue(country)
+            self.widgets['area'].setRenderedValue(area)
+            self.widgets['extension'].setRenderedValue(extension)
+            return
+        return
+
+    def setPrefix(self, prefix):
+        """See zope.formlib.interfaces.IWidget"""
+        # Set the prefix locally
+        if not prefix.endswith("."):
+            prefix += '.'
+        self._prefix = prefix
+        self.name = prefix + self.context.__name__
+        # Now distribute it to the sub-widgets
+        self._generateSubWidgets()
+        return
+
+    def getInputValue(self):
+        """See zope.formlib.interfaces.IInputWidget"""
+        self._error = None
+        try:
+            result = u'-'.join((
+                self.widgets['country'].getInputValue(),
+                self.widgets['area'].getInputValue(),
+                self.widgets['extension'].getInputValue() ))
+            return result
+        except TypeError:
+            if self.required:
+                return self._missing
+            return None
+        except ValueError, v:
+            self._error = WidgetInputError(
+                self.context.__name__, self.label, _(v))
+            raise self._error
+        except WidgetInputError, e:
+            self._error = e
+            raise e
+
+    def applyChanges(self, content):
+        """See zope.formlib.interfaces.IInputWidget"""
+        field = self.context
+        new_value = self.getInputValue()
+        old_value = field.query(content, self)
+        # The selection has not changed
+        if new_value == old_value:
+            return False
+        field.set(content, new_value)
+        return True
+
+    def hasInput(self):
+        """See zope.formlib.interfaces.IInputWidget"""
+        return (self.widgets['country'].hasInput() and
+                (self.widgets['area'].hasInput() and
+                 self.widgets['extension'].hasInput()))
+
+
+    def hasValidInput(self):
+        """See zope.formlib.interfaces.IInputWidget"""
+        return (self.widgets['country'].hasValidInput() and
+            self.widgets['area'].hasValidInput() and
+            self.widgets['extension'].hasValidInput())
+
+
+    def hidden(self):
+        """See zope.formlib.interfaces.IBrowserWidget"""
+        output = []
+        output.append(self.widgets['country'].hidden())
+        output.append(self.widgets['area'].hidden())
+        output.append(self.widgets['extension'].hidden())
+        return '\n'.join(output)
+
+
+    def error(self):
+        """See zope.formlib.interfaces.IBrowserWidget"""
+        if self._error:
+            return getMultiAdapter(
+                (self._error, self.request),
+                IWidgetInputErrorView).snippet()
+        country_error = self.widgets['country'].error()
+        if country_error:
+            return country_error
+        area_error = self.widgets['area'].error()
+        if area_error:
+            return area_error
+        extension_error = self.widgets['extension'].error()
+        if extension_error:
+            return extension_error
+        return ""
+
+    def __call__(self):
+        """See zope.formlib.interfaces.IBrowserWidget"""
+        return self.template()
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/restwidget.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/restwidget.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/restwidget.py	(revision 7760)
@@ -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.sirp.utils.helpers import ReST2HTML
+from waeup.sirp.interfaces import ISIRPUtils
+
+
+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.SIRPUtils.
+        """
+        if self._renderedValueSet():
+            value = self._data
+        else:
+            value = self.context.default
+        if value == self.context.missing_value:
+            return {}
+        parts = value.split('>>')
+        elements = {}
+        lang = getUtility(ISIRPUtils).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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/tests/__init__.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/tests/__init__.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/tests/__init__.py	(revision 7760)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/tests/test_datewidget.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/tests/test_datewidget.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/tests/test_datewidget.py	(revision 7760)
@@ -0,0 +1,137 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.verify import verifyClass
+
+from zope.formlib.tests.test_browserwidget import (
+    SimpleInputWidgetTest, BrowserWidgetTest, )
+from zope.formlib.interfaces import IInputWidget, IDisplayWidget
+from waeup.sirp.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 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 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')
+
+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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/tests/test_objectwidget.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/tests/test_objectwidget.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/tests/test_objectwidget.py	(revision 7760)
@@ -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
+##
+"""Tests for objectwidget.
+"""
+import doctest
+import unittest
+import sys
+from zope.component import testing
+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.sirp.widgets.objectwidget import SIRPObjectWidget as ObjectWidget
+from waeup.sirp.widgets.objectwidget import (
+    SIRPObjectDisplayWidget 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):
+        from zope.interface.verify import DoesNotImplement
+        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 = [
+            '<td', 'class=', 'row',
+            'Foo Name',
+            '</td>', '<td', 'class=', 'row',
+            'foo@foo.test',
+            '</td>']
+        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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/tests/test_phonewidget.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/tests/test_phonewidget.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/widgets/tests/test_phonewidget.py	(revision 7760)
@@ -0,0 +1,498 @@
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.
+
+Tests in here are an extended reformulation of the tests done in
+z3.widget as doctests for the usphone widget.
+"""
+import unittest
+from zope import schema
+# XXX: could we possibly get rid of zope.app.testing?
+from zope.app.testing.setup import placefulSetUp, placefulTearDown
+from zope.component import provideAdapter
+from zope.formlib.exception import WidgetInputErrorView
+from zope.formlib.interfaces import (
+    WidgetInputError, IInputWidget, IBrowserWidget, IWidgetInputError,
+    IWidgetInputErrorView)
+from zope.formlib.textwidgets import TextWidget
+from zope.interface import verify, Interface, implements
+from zope.publisher.browser import TestRequest
+from zope.publisher.interfaces.browser import IDefaultBrowserLayer
+from zope.schema.interfaces import ITextLine
+from waeup.sirp.widgets.phonewidget import PhoneWidget
+
+# Dummy content
+class ISampleContent(Interface):
+    foo = schema.TextLine(
+        title = u'Phone',
+        description = u'Phone number',
+        required = True)
+
+    bar = schema.TextLine(
+        title = u'Phone',
+        description = u'Phone number (not required)',
+        required = False,)
+
+    baz = schema.TextLine(
+        title = u'Phone',
+        description = u'Required phone with a default',
+        required = True,
+        default=u'234--')
+
+class SampleContent:
+    implements(ISampleContent)
+
+class PhoneWidgetTests(unittest.TestCase):
+    # Tests for phone widget
+
+    def setUp(self):
+        placefulSetUp()
+
+        # register TextLine views (so that subwidgets can be rendered)
+        provideAdapter(
+            TextWidget, (ITextLine, IDefaultBrowserLayer), IInputWidget)
+        # errors in forms
+        provideAdapter(
+            WidgetInputErrorView, (IWidgetInputError, IDefaultBrowserLayer),
+            IWidgetInputErrorView)
+
+        # setup some sample content to bind to (only neccessary for
+        # some of the tests)
+        self.content = SampleContent()
+        self.field = ISampleContent['foo']
+        self.field = self.field.bind(self.content)
+        self.field_nonreq = ISampleContent['bar']
+        self.field_nonreq = self.field_nonreq.bind(self.content)
+        self.field_w_default = ISampleContent['baz']
+        self.field_w_default = self.field_w_default.bind(self.content)
+
+        # create an empty request. We need one to create a widget
+        self.request = TestRequest()
+        self.widget = PhoneWidget(self.field, self.request)
+        self.widget_nonreq = PhoneWidget(self.field_nonreq, self.request)
+        return
+
+    def tearDown(self):
+        placefulTearDown()
+        return
+
+    def test_ifaces(self):
+        # make sure we fullfill interface contracts
+        verify.verifyClass(IBrowserWidget, PhoneWidget)
+        verify.verifyClass(IInputWidget, PhoneWidget)
+        verify.verifyObject(IBrowserWidget, self.widget)
+        verify.verifyObject(IInputWidget, self.widget)
+        return
+
+    def test_attribs_set(self):
+        # make sure the public attributes are set correctly
+        self.assertEqual(self.widget.name, 'field.foo')
+        self.assertEqual(self.widget.label, u'Phone')
+        self.assertEqual(self.widget.hint, u'Phone number')
+        self.assertEqual(self.widget.visible, True)
+        self.assertEqual(self.widget.required, True)
+        self.assertEqual(self.widget_nonreq.required, False)
+        return
+
+    def test_subwidgets_exist(self):
+        # make sure we have three sub widgets
+        sub_widget1 = self.widget.widgets['country']
+        sub_widget2 = self.widget.widgets['area']
+        sub_widget3 = self.widget.widgets['extension']
+        self.assertTrue(isinstance(sub_widget1, TextWidget))
+        self.assertTrue(isinstance(sub_widget2, TextWidget))
+        self.assertTrue(isinstance(sub_widget3, TextWidget))
+        return
+
+    def test_set_rendered_value(self):
+        # make sure we render values correctly
+        self.widget.setRenderedValue(u'123-456-1234567')
+        self.assertEqual(
+            self.widget(),
+            (u'+&nbsp;<input class="span2 textType" id="field.foo.country"'
+             u' name="field.foo.country" size="20" type="text" value="123" '
+             u' />'
+             u'&nbsp;-\n'
+             u'<input class="span2 textType" id="field.foo.area"'
+             u' name="field.foo.area" size="20" type="text" value="456" '
+             u' />'
+             u'&nbsp;-\n'
+             u'<input class="span3 textType" id="field.foo.extension"'
+             u' name="field.foo.extension" size="20" type="text"'
+             u' value="1234567"  />\n'))
+        return
+
+    def test_set_prefix_default(self):
+        # prefix determines the name of widget and its subwidgets.
+        self.assertEqual(self.widget.name, 'field.foo')
+        self.assertEqual(self.widget.widgets['country'].name,
+                         'field.foo.country')
+        self.assertEqual(self.widget.widgets['area'].name,
+                         'field.foo.area')
+        self.assertEqual(self.widget.widgets['extension'].name,
+                         'field.foo.extension')
+        return
+
+    def test_set_prefix_changed(self):
+        # when we set the prefix, widget names change
+        self.widget.setPrefix('test.')
+        self.assertEqual(self.widget.name, 'test.foo')
+        self.assertEqual(self.widget.widgets['country'].name,
+                         'test.foo.country')
+        self.assertEqual(self.widget.widgets['area'].name,
+                         'test.foo.area')
+        self.assertEqual(self.widget.widgets['extension'].name,
+                         'test.foo.extension')
+        return
+
+    def test_set_prefix_changed_no_dot(self):
+        # when we try to set a name without trailing dot, one will be
+        # inserted automatically
+        self.widget.setPrefix('test')
+        self.assertEqual(self.widget.name, 'test.foo')
+        self.assertEqual(self.widget.widgets['country'].name,
+                         'test.foo.country')
+        self.assertEqual(self.widget.widgets['area'].name,
+                         'test.foo.area')
+        self.assertEqual(self.widget.widgets['extension'].name,
+                         'test.foo.extension')
+        return
+
+    def test_get_input_value(self):
+        # we get a string when we want the input value
+        request = TestRequest(
+            form={
+                'field.foo.country': '123',
+                'field.foo.area': '456',
+                'field.foo.extension': '12345'
+                })
+        widget = PhoneWidget(self.field, request)
+        value = widget.getInputValue()
+        self.assertEqual(value, '123-456-12345')
+        return
+
+    def test_get_input_value_invalid(self):
+        # we get errors when valid data is sent
+        request = TestRequest(
+            form={
+                'field.foo.country': 'not-a-number',
+                'field.foo.area': '456',
+                'field.foo.extension': '12345',
+                })
+        widget = PhoneWidget(self.field, request)
+        self.assertRaises(
+            WidgetInputError,
+            widget.getInputValue)
+        return
+
+    def test_apply_changes(self):
+        # we can apply passed-in numbers to content objects. The
+        # widget is smart enough to detect whether the values really
+        # changed.
+        class Content(object):
+            field = None
+        content = Content()
+        request = TestRequest(form={
+                'field.foo.country': '123',
+                'field.foo.area': '456',
+                'field.foo.extension': '7890'})
+        widget = PhoneWidget(self.field, request)
+        result1 = widget.applyChanges(content)
+        result2 = widget.applyChanges(content)
+        self.assertEqual(result1, True)
+        self.assertEqual(content.foo, u'123-456-7890')
+        self.assertEqual(result2, False)
+        return
+
+    def test_has_input(self):
+        # we can check whether there is input without actually validating.
+        request = TestRequest(form={
+                'field.foo.country': '123'})
+        widget = PhoneWidget(self.field, request)
+        result = widget.hasInput()
+        self.assertEqual(result, False)
+
+        request = TestRequest(form={
+                'field.foo.area': '123'})
+        widget = PhoneWidget(self.field, request)
+        result = widget.hasInput()
+        self.assertEqual(result, False)
+
+        request = TestRequest(form={
+                'field.foo.extension': '123567'})
+        widget = PhoneWidget(self.field, request)
+        result = widget.hasInput()
+        self.assertEqual(result, False)
+
+        request = TestRequest(form={
+                'field.foo.country': '123',
+                'field.foo.area': '456',
+                'field.foo.extension': '7890'})
+        widget = PhoneWidget(self.field, request)
+        result = widget.hasInput()
+        self.assertEqual(result, True)
+
+        self.assertEqual(self.widget.hasInput(), False)
+        return
+
+    def test_has_valid_input(self):
+        # we can check for input also including validation
+        request = TestRequest(form={
+                'field.foo.country': '123'})
+        widget = PhoneWidget(self.field, request)
+        result = widget.hasValidInput()
+        self.assertEqual(result, False)
+
+        request = TestRequest(form={
+                'field.foo.area': '123'})
+        widget = PhoneWidget(self.field, request)
+        result = widget.hasValidInput()
+        self.assertEqual(result, False)
+
+        request = TestRequest(form={
+                'field.foo.extension': '123567'})
+        widget = PhoneWidget(self.field, request)
+        result = widget.hasValidInput()
+        self.assertEqual(result, False)
+
+        request = TestRequest(form={
+                'field.foo.country': '123',
+                'field.foo.area': '456',
+                'field.foo.extension': '7890'})
+        widget = PhoneWidget(self.field, request)
+        result = widget.hasValidInput()
+        self.assertEqual(result, True)
+
+        self.assertEqual(self.widget.hasValidInput(), False)
+        return
+
+    def test_hidden(self):
+        # we can render output as hidden field(s)
+        self.widget.setRenderedValue(u'123-456-1234567')
+        self.assertEqual(
+            self.widget.hidden(),
+            (u'<input class="span2 hiddenType" id="field.foo.country"'
+             u' name="field.foo.country" type="hidden" value="123" '
+             u' />'
+             u'\n'
+             u'<input class="span2 hiddenType" id="field.foo.area"'
+             u' name="field.foo.area" type="hidden" value="456" '
+             u' />'
+             u'\n'
+             u'<input class="span3 hiddenType" id="field.foo.extension"'
+             u' name="field.foo.extension" type="hidden"'
+             u' value="1234567"  />'))
+        return
+
+    def test_error_extension(self):
+        # the widget can handle input errors in extension subwidget
+
+        # extension field too short
+        request = TestRequest(form={
+                'field.foo.country': '123',
+                'field.foo.area': '456',
+                'field.foo.extension': '0'})
+        widget = PhoneWidget(self.field, request)
+        try:
+            widget.getInputValue()
+            self.fail('No error raised')
+        except WidgetInputError, e:
+            pass
+        self.assertEqual(
+            e.__repr__(),
+            ("WidgetInputError("
+             "'extension', u'Direct Line', ConstraintNotSatisfied(u'0'))"))
+        self.assertEqual(
+            widget.error(),
+            u'<span class="error">Constraint not satisfied</span>')
+        return
+
+
+    def test_error_area(self):
+        # the widget can handle input errors in area code subwidget
+
+        # area code contains illegal chars
+        request = TestRequest(form={
+                'field.foo.country': '123',
+                'field.foo.area': 'no-number',
+                'field.foo.extension': '12345'})
+        widget = PhoneWidget(self.field, request)
+        try:
+            widget.getInputValue()
+            self.fail('No error raised')
+        except WidgetInputError, e:
+            pass
+        self.assertEqual(
+            e.__repr__(),
+            ("WidgetInputError("
+             "'area', u'Area Code', ConstraintNotSatisfied(u'no-number'))"))
+        self.assertEqual(
+            widget.error(),
+            u'<span class="error">Constraint not satisfied</span>')
+        return
+
+    def test_error_country(self):
+        # the widget can handle input errors in country code subwidget
+
+        # invalid country code
+        request = TestRequest(form={
+                'field.foo.country': 'XXX',
+                'field.foo.area': '456',
+                'field.foo.extension': '12345'})
+        widget = PhoneWidget(self.field, request)
+        try:
+            widget.getInputValue()
+            self.fail('No error raised')
+        except WidgetInputError, e:
+            pass
+        self.assertEqual(
+            e.__repr__(),
+            ("WidgetInputError("
+             "'country', u'Country Code', ConstraintNotSatisfied(u'XXX'))"))
+        self.assertEqual(
+            widget.error(),
+            u'<span class="error">Constraint not satisfied</span>')
+        return
+
+    def test_render_wo_data(self):
+        # when no data was submitted, the content data will be displayed
+        request = TestRequest(form={})
+        self.content.foo = u'11-222-3333'
+        widget = PhoneWidget(self.field, request)
+        self.assertEqual(
+            widget(),
+            (u'+&nbsp;<input class="span2 textType" id="field.foo.country"'
+             u' name="field.foo.country" size="20" type="text" value="11" '
+             u' />'
+             u'&nbsp;-\n'
+             u'<input class="span2 textType" id="field.foo.area"'
+             u' name="field.foo.area" size="20" type="text" value="222" '
+             u' />'
+             u'&nbsp;-\n'
+             u'<input class="span3 textType" id="field.foo.extension"'
+             u' name="field.foo.extension" size="20" type="text"'
+             u' value="3333"  />\n'))
+        return
+
+    def test_render_with_data(self):
+        # when data was in the request, it will override any content value
+        request = TestRequest(form={
+                'field.foo.country': '11',
+                'field.foo.area': '222',
+                'field.foo.extension': '3333'})
+        self.content.foo = u'22-333-4444'
+        widget = PhoneWidget(self.field, request)
+        self.assertEqual(
+            widget(),
+            (u'+&nbsp;<input class="span2 textType" id="field.foo.country"'
+             u' name="field.foo.country" size="20" type="text" value="11" '
+             u' />'
+             u'&nbsp;-\n'
+             u'<input class="span2 textType" id="field.foo.area"'
+             u' name="field.foo.area" size="20" type="text" value="222" '
+             u' />'
+             u'&nbsp;-\n'
+             u'<input class="span3 textType" id="field.foo.extension"'
+             u' name="field.foo.extension" size="20" type="text"'
+             u' value="3333"  />\n'))
+        return
+
+    def test_render_changed_prefix(self):
+        # when the prefix is changed, the subwidgets will reflect that
+        # properly. This tests a regression: if setPrefix was called
+        # on the phone widget, the subwidgets were already assuming
+        # that there is no data in the request
+        request = TestRequest(form={
+                'field.foo.country': '11',
+                'field.foo.area': '222',
+                'field.foo.extension': '3333'})
+        self.content.foo = u'22-333-4444'
+        widget = PhoneWidget(self.field, request)
+        # when setting a wrong prefix (not in the form), subwidgets
+        # will display the content values assuming (correctly) there
+        # is no data sent for them in the request.
+        widget.setPrefix('not-existent.')
+        self.assertTrue('value="4444"' in widget())
+        # when setting the right prefix, subwidgets will display the
+        # sent form values and not the content values.
+        widget.setPrefix('field.')
+        self.assertTrue('value="3333"' in widget())
+        return
+
+    def test_render_changed_prefix(self):
+        # when the prefix is changed, the subwidgets will reflect that
+        # properly. This tests a regression: if setPrefix was called
+        # on the phone widget, the subwidgets were already assuming
+        # that there is no data in the request
+        request = TestRequest(form={
+                'field.foo.country': '11',
+                'field.foo.area': '222',
+                'field.foo.extension': '3333'})
+        self.content.foo = u'22-333-4444'
+        widget = PhoneWidget(self.field, request)
+        # when setting a wrong prefix (not in the form), subwidgets
+        # will display the content values assuming (correctly) there
+        # is no data sent for them in the request.
+        widget.setPrefix('not-existent.')
+        self.assertTrue('value="4444"' in widget())
+        # when setting the right prefix, subwidgets will display the
+        # sent form values and not the content values.
+        widget.setPrefix('field.')
+        self.assertTrue('value="3333"' in widget())
+        return
+
+    def test_non_required_no_input(self):
+        # if the bound field requires no input phone widget will cope
+        # with that.
+        class Content(object):
+            field = None
+
+        content = Content()
+        request = TestRequest(form={
+                'field.bar.country': '',
+                'field.bar.area': '',
+                'field.bar.extension': '',
+                })
+        widget = PhoneWidget(self.field_nonreq, request)
+        result1 = widget.applyChanges(content)
+        result2 = widget.getInputValue()
+        self.assertEqual(result1, True)
+        # without input we get None
+        self.assertTrue(content.bar is None)
+        self.assertTrue(result2 is None)
+        return
+
+    def test_set_rendered_with_empty_context_value(self):
+        # if we set empty string as rendered value, empty fields are
+        # displayed.
+        self.widget.setRenderedValue(u'')
+        self.assertEqual(
+            self.widget(),
+            (u'+&nbsp;<input class="span2 textType" id="field.foo.country"'
+             u' name="field.foo.country" size="20" type="text" value="" '
+             u' />'
+             u'&nbsp;-\n'
+             u'<input class="span2 textType" id="field.foo.area"'
+             u' name="field.foo.area" size="20" type="text" value="" '
+             u' />'
+             u'&nbsp;-\n'
+             u'<input class="span3 textType" id="field.foo.extension"'
+             u' name="field.foo.extension" size="20" type="text"'
+             u' value=""  />\n'))
+        return
Index: main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/workflow.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/workflow.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/workflow.py	(revision 7760)
@@ -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.sirp.interfaces import ISIRPWorkflowInfo
+
+class SIRPWorkflow(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 SIRPWorkflowInfo(WorkflowInfo):
+    """A workflow info that provides a convenience transition getter.
+    """
+
+    grok.provides(ISIRPWorkflowInfo)
+
+    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.sirp/branches/ulif-schoolgrades/src/waeup/sirp/zcml.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/zcml.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/src/waeup/sirp/zcml.py	(revision 7760)
@@ -0,0 +1,54 @@
+##
+## zcml.py
+## Login : <uli@pu.smp.net>
+## Started on  Fri Feb  3 04:26:49 2012 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2012 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
+##
+from zope.component.zcml import handler
+from waeup.sirp.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:sirp="http://namespaces.waeup.org/sirp"``
+
+    - Then, after including waeup.sirp:
+        ``<sirp: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.sirp.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.sirp/branches/ulif-schoolgrades/tools/fix_import_file.py
===================================================================
--- main/waeup.sirp/branches/ulif-schoolgrades/tools/fix_import_file.py	(revision 7760)
+++ main/waeup.sirp/branches/ulif-schoolgrades/tools/fix_import_file.py	(revision 7760)
@@ -0,0 +1,325 @@
+## $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 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.
+"""
+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',
+    'birthday': 'date',
+    'request_date': 'datetime',
+    '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',
+    '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',
+    }
+
+# Mapping input file colnames --> output file colnames
+COLNAME_MAPPING = {
+    # base data
+    'jamb_reg_no': 'reg_number',
+    'matric_no': 'matric_number',
+    'birthday': 'date_of_birth',
+    'clr_ac_pin': 'clr_code',
+    # 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',
+    }
+
+# Mapping input regh_state --> output reg_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',
+    }
+
+##
+## 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
+
+class Converters():
+    """Converters to turn old-style values into new ones.
+    """
+    @classmethod
+    def reg_state(self, value):
+        """ 'courses_validated' --> 'courses validated'
+        """
+        return REGSTATE_MAPPING.get(value,value)
+
+    @classmethod
+    def level(self, value):
+        """ '000' --> '10'
+        """
+        try:
+            number = int(value)
+        except ValueError:
+            return 9999
+        if number == 0:
+            return 10
+        return number
+
+    @classmethod
+    def semester(self, value):
+        """ '0' --> '9'
+        """
+        try:
+            number = int(value)
+        except ValueError:
+            return 9999
+        if number == 0:
+            return 9
+        return number
+
+    @classmethod
+    def application_category(self, value):
+        """ '' --> 'no'
+        """
+        if value == '':
+            return 'no'
+        return value
+
+    @classmethod
+    def lga(self, value):
+        """ Remove apostrophe
+        """
+        if value == 'akwa_ibom_uru_offong_oruko':
+            return 'akwa_ibom_urue-offong-oruko'
+        try:
+            value = value.replace("'","")
+            value = value.lower()
+        except:
+            return ''
+        return value
+
+
+    @classmethod
+    def session(self, value):
+        """ '08' --> '2008'
+        """
+        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 marit_stat(self, value):
+        """ '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):
+        """ 'True'/'False' --> 'f'/'m'
+        """
+        if value in ('True','f'):
+            value = 'f'
+        elif value in ('False','m'):
+            value = 'm'
+        else:
+            value = ''
+        return value
+
+    @classmethod
+    def date(self, value):
+        """ '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):
+        """ Add hash.
+        """
+        # We add the hash symbol to avoid automatic number transformation
+        # in Excel and Calc for further processing
+        value += '#'
+        return value
+
+
+    @classmethod
+    def datetime(self, value):
+        """ 'yyyy/mm/dd' --> 'yyyy-mm-dd'
+        """
+        #print  "IN: ", value
+        if value == "None":
+            value = ""
+        elif value == "":
+            value = ""
+        else:
+            #value = datetime.datetime.strptime(value, '%Y/%m/%d')
+            #value = datetime.datetime.strftime(value, '%Y-%m-%d')
+            pass
+        #print "OUT: ", value
+        return value
+
+    @classmethod
+    def mode(self, value):
+        if value == "transfer_fulltime":
+            return "transfer_ft"
+        return value
+
+    @classmethod
+    def password(self, value):
+        if value == "not set":
+            return ""
+        return value
+
+    @classmethod
+    def phone(self, value):
+        """ '<num-seq1>-<num-seq2> asd' -> '--<num-seq1><num-seq2>'
+
+        Dashes and slashes are removed before looking for sequences
+        of numbers.
+        """
+        value = value.replace('-', '')
+        value = value.replace('/', '')
+        match = RE_PHONE.match(value)
+        phone = match.groups()[0]
+        value = '--%s' % phone
+        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)
+        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['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()
