Index: /main/waeup.sirp/branches/uli-studentpw/CHANGES.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/CHANGES.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/CHANGES.txt	(revision 6703)
@@ -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/uli-studentpw/LICENSE
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/LICENSE	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/LICENSE	(revision 6703)
@@ -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/uli-studentpw/README.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/README.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/README.txt	(revision 6703)
@@ -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/uli-studentpw/buildout-windows.cfg
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/buildout-windows.cfg	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/buildout-windows.cfg	(revision 6703)
@@ -0,0 +1,104 @@
+[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]
+#zc.sourcefactory = 0.3.5
+#zope.testing = 3.7.0
+z3c.testsetup = 0.6.1
+hurry.workflow = 0.10
+#hurry.query = 0.9.2
+zope.xmlpickle = 3.4.0
+#hurry.yui = 2.7.0
+hurry.zoperesource = 0.6 # Pinned to circumvent breakage in 0.4.x
+megrok.layout = 1.0.2
+lovely.recipe = 1.0.0    # Pinned to prevent buildout svn-error.
+collective.recipe.sphinxbuilder = 0.7.0
+reportlab = 2.5
+# Require lates version...
+Sphinx = 
+
+[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}
+
+[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/uli-studentpw/buildout.cfg
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/buildout.cfg	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/buildout.cfg	(revision 6703)
@@ -0,0 +1,172 @@
+[buildout]
+develop = .
+parts =
+    eggbasket
+    app
+    i18n
+    test
+    mkdirs
+    zpasswd
+    zope_conf
+    site_zcml
+    zdaemon_conf
+    deploy_ini
+    debug_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 =
+ZODB3 = 3.10.3
+
+[app]
+# This creates all scripts in bin/. The sirpctl created here is 'faulty'.
+recipe = zc.recipe.egg
+eggs = waeup.sirp
+       z3c.evalexception>=2.0
+       Paste
+       PasteScript
+       PasteDeploy
+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
+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}
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = waeup.sirp [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
+
+[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
+
+[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
+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/uli-studentpw/docs/INSTALL.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/INSTALL.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/INSTALL.txt	(revision 6703)
@@ -0,0 +1,84 @@
+Installation on Linux production system
+***************************************
+
+.. note:: `waeup.sirp` might not work with Python > 2.6
+          currently. Use of Python 2.6 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/uli-studentpw/docs/INSTALLWIN.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/INSTALLWIN.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/INSTALLWIN.txt	(revision 6703)
@@ -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/uli-studentpw/docs/Makefile
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/Makefile	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/Makefile	(revision 6703)
@@ -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/uli-studentpw/docs/make.bat
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/make.bat	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/make.bat	(revision 6703)
@@ -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/uli-studentpw/docs/source/_templates/mainpage.template
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/_templates/mainpage.template	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/_templates/mainpage.template	(revision 6703)
@@ -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/uli-studentpw/docs/source/bugs.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/bugs.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/bugs.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/changes.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/changes.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/changes.rst	(revision 6703)
@@ -0,0 +1,1 @@
+.. include:: ../../CHANGES.txt
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/conf.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/conf.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/conf.py	(revision 6703)
@@ -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/uli-studentpw/docs/source/contents.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/contents.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/contents.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/copyright.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/copyright.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/copyright.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_accesscode.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_accesscode.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_accesscode.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`accesscodes.accesscode` -- AC Components
+*****************************************
+
+.. automodule:: waeup.sirp.accesscodes.accesscode
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_browser.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_browser.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_browser.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_catalog.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_catalog.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_catalog.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`accesscodes.catalog` -- AC Catalog Components
+**********************************************
+
+.. automodule:: waeup.sirp.accesscodes.catalog
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_interfaces.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_interfaces.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_interfaces.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_workflow.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_workflow.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/accesscodes/api_workflow.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`accesscodes.workflow` - AC Workflow Components
+***********************************************
+
+.. automodule:: waeup.sirp.accesscodes.workflow
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_accesscodes.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_accesscodes.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_accesscodes.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/developerdocs/api/api_app.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_app.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_app.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`app` -- University Root (Site) Module
+**************************************
+
+.. automodule:: waeup.sirp.app
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_applicants.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_applicants.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_applicants.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/developerdocs/api/api_authentication.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_authentication.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_authentication.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`authentication` -- Authentication Components Module
+****************************************************
+
+.. automodule:: waeup.sirp.authentication
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_browser.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_browser.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_browser.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/developerdocs/api/api_catalog.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_catalog.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_catalog.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`catalog` -- Cataloging Components Module
+*****************************************
+
+.. automodule:: waeup.sirp.catalog
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_datacenter.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_datacenter.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_datacenter.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`datacenter` -- Datacenter Module
+*********************************
+
+.. automodule:: waeup.sirp.datacenter
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_interfaces.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_interfaces.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_interfaces.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/developerdocs/api/api_permissions.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_permissions.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_permissions.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`permissions` -- Permissions and Roles Components Module
+********************************************************
+
+.. automodule:: waeup.sirp.permissions
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_university.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_university.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_university.rst	(revision 6703)
@@ -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_facultycontainer
+   university/api_faculty
+   university/api_department
+   university/api_certificatecontainer
+   university/api_coursecontainer
+   university/api_course
+   university/api_certificate
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_users.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_users.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_users.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`users` -- Users (Principals) Module
+************************************
+
+.. automodule:: waeup.sirp.users
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_utils.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_utils.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_utils.rst	(revision 6703)
@@ -0,0 +1,19 @@
+`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
+
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_workflow.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_workflow.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/api_workflow.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`workflow` -- Workflow Components Module
+****************************************
+
+.. automodule:: waeup.sirp.workflow
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_applicant.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_applicant.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_applicant.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`applicants.applicant` - Applicant Components
+*********************************************
+
+.. automodule:: waeup.sirp.applicants.applicant
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_authentication.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_authentication.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_authentication.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`applicants.authentication` - Applicants Authentication Components
+******************************************************************
+
+.. automodule:: waeup.sirp.applicants.authentication
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_batching.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_batching.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_batching.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`applicants.batching` - Batch Importer for Applicants
+*****************************************************
+
+.. automodule:: waeup.sirp.applicants.batching
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_browser.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_browser.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_browser.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`applicants.browser` - Applicants UI Components
+***********************************************
+
+.. automodule:: waeup.sirp.applicants.browser
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_container.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_container.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_container.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`applicants.container` - Container for Applicants
+*************************************************
+
+.. automodule:: waeup.sirp.applicants.container
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_interfaces.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_interfaces.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_interfaces.rst	(revision 6703)
@@ -0,0 +1,48 @@
+`applicants.interfaces` - Applicants Related Interfaces
+*******************************************************
+
+.. automodule:: waeup.sirp.applicants.interfaces
+   :members:
+   :undoc-members:
+
+   .. autointerface:: IResultEntry
+      :members:
+
+   .. autointerface:: IApplicantsRoot
+      :members:
+
+   .. autointerface:: IApplicantsContainer
+      :members:
+
+   .. autointerface:: IApplicantsContainerAdd
+      :members:
+
+   .. autointerface:: IApplicantBaseData
+      :members:
+
+   .. autointerface:: IApplicant
+      :members:
+
+   .. autointerface:: IApplicantEdit
+      :members:
+
+   .. autointerface:: IResultEntry
+      :members:
+
+   .. autointerface:: IApplicantPrincipalInfo
+      :members:
+
+   .. autointerface:: IApplicantPrincipal
+      :members:
+
+   .. autointerface:: IApplicantPrincipal
+      :members:
+
+   .. autointerface:: IApplicantsFormChallenger
+      :members:
+
+   .. autointerface:: IApplicantSessionCredentials
+      :members:
+
+   .. autointerface:: IApplicantsContainerProvider
+      :members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_permissions.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_permissions.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_permissions.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`applicants.permissions` - Applicants Permission Components
+***********************************************************
+
+.. automodule:: waeup.sirp.applicants.permissions
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_root.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_root.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_root.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`applicants.root` - Root for Applicants Containers
+**************************************************
+
+.. automodule:: waeup.sirp.applicants.root
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_vocabularies.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_vocabularies.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_vocabularies.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`applicants.vocabularies` - Vocabularies and Sources
+****************************************************
+
+.. automodule:: waeup.sirp.applicants.vocabularies
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_workflow.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_workflow.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/applicants/api_workflow.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`applicants.workflow` - Applicants Workflow Components
+******************************************************
+
+.. automodule:: waeup.sirp.applicants.workflow
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_breadcrumbs.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_breadcrumbs.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_breadcrumbs.rst	(revision 6703)
@@ -0,0 +1,7 @@
+`browser.breadcrumbs` - Browser Breadcrumbs
+*******************************************
+
+.. automodule:: waeup.sirp.browser.breadcrumbs
+   :members:
+   :undoc-members:
+
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_exceptions.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_exceptions.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_exceptions.rst	(revision 6703)
@@ -0,0 +1,7 @@
+`browser.exceptions` - Browser Exceptions
+*****************************************
+
+.. automodule:: waeup.sirp.browser.exceptions
+   :members:
+   :undoc-members:
+
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_interfaces.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_interfaces.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_interfaces.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/developerdocs/api/browser/api_layout.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_layout.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_layout.rst	(revision 6703)
@@ -0,0 +1,7 @@
+`browser.layout` - Browser Layout
+*********************************
+
+.. automodule:: waeup.sirp.browser.layout
+   :members:
+   :undoc-members:
+
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_pages.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_pages.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_pages.rst	(revision 6703)
@@ -0,0 +1,7 @@
+`browser.pages` - Browser Pages (Views)
+***************************************
+
+.. automodule:: waeup.sirp.browser.pages
+   :members:
+   :undoc-members:
+
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_resources.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_resources.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_resources.rst	(revision 6703)
@@ -0,0 +1,7 @@
+`browser.resources` - Browser Resources
+***************************************
+
+.. automodule:: waeup.sirp.browser.resources
+   :members:
+   :undoc-members:
+
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_theming.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_theming.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_theming.rst	(revision 6703)
@@ -0,0 +1,7 @@
+`browser.theming` - Browser Theming
+***********************************
+
+.. automodule:: waeup.sirp.browser.theming
+   :members:
+   :undoc-members:
+
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_viewlets.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_viewlets.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/browser/api_viewlets.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/developerdocs/api/index.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/index.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/index.rst	(revision 6703)
@@ -0,0 +1,28 @@
+API
+***
+
+.. automodule:: waeup.sirp
+   :members:
+
+Modules and Packages
+--------------------
+
+`waeup.sirp` provides the following modules and subpackages:
+
+.. toctree::
+   :maxdepth: 1
+
+   api_app
+   api_interfaces
+   api_authentication
+   api_catalog
+   api_permissions
+   api_datacenter
+   api_users
+   api_workflow
+   api_university
+   api_browser
+   api_applicants
+   api_accesscodes
+   api_utils
+
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_batching.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_batching.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_batching.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`university.batching` - Importer Components
+*******************************************
+
+.. automodule:: waeup.sirp.university.batching
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_catalog.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_catalog.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_catalog.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`university.catalog` - Cataloging Components
+********************************************
+
+.. automodule:: waeup.sirp.university.catalog
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_certificate.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_certificate.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_certificate.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`university.certificate` - Certificate Components
+*************************************************
+
+.. automodule:: waeup.sirp.university.certificate
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_certificatecontainer.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_certificatecontainer.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_certificatecontainer.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`university.certificatecontainer` - Certificate Container Components
+********************************************************************
+
+.. automodule:: waeup.sirp.university.certificatecontainer
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_course.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_course.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_course.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`university.course` - Course Components
+***************************************
+
+.. automodule:: waeup.sirp.university.course
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_coursecontainer.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_coursecontainer.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_coursecontainer.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`university.coursecontainer` - Course Container Components
+**********************************************************
+
+.. automodule:: waeup.sirp.university.coursecontainer
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_department.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_department.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_department.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`university.department` - Department Components
+***********************************************
+
+.. automodule:: waeup.sirp.university.department
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_faculty.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_faculty.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_faculty.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`university.faculty` - Faculty Components
+*****************************************
+
+.. automodule:: waeup.sirp.university.faculty
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_facultycontainer.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_facultycontainer.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_facultycontainer.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`university.facultycontainer` - Faculty Container Components
+************************************************************
+
+.. automodule:: waeup.sirp.university.facultycontainer
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_interfaces.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_interfaces.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_interfaces.rst	(revision 6703)
@@ -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.IFacultyContainer
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.IDepartment
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.IDepartmentAdd
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.ICourseContainer
+   :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.ICertificateContainer
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.ICertificateCourse
+   :members:
+
+.. autointerface:: waeup.sirp.university.interfaces.ICertificateCourseAdd
+   :members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_vocabularies.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_vocabularies.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/api/university/api_vocabularies.rst	(revision 6703)
@@ -0,0 +1,6 @@
+`university.vocabularies` - Vocabularies and Sources
+****************************************************
+
+.. automodule:: waeup.sirp.university.vocabularies
+   :members:
+   :undoc-members:
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/developernotes.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/developernotes.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/developernotes.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/developerdocs/german/accesscodes.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/accesscodes.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/accesscodes.rst	(revision 6703)
@@ -0,0 +1,24 @@
+Access Codes
+------------
+
+Die Access Codes (AC) dienen zwei Zwecken. Zum einen ermöglicht der Verkauf dieser Codes auf Scratch Cards die Bezahlung von Dienstleistungen ohne Verwendung des aufwendigen Cash Card Systems. Zum anderen kann über die persönlich ausgegebenen Codes (beim Verkauf wird zu jeder Seriennummer der Name notiert) der sichere Erstzugang zum Portal ermöglicht werden. Der Zufallsteil des Codes wird bei der Application als geheime Zugangskennung zum persönlichen Datensatz und beim Admission Checking als Erstpasswort für das Portal verwendet. Das AC-Modul hat folgende Eigenschaften:
+
+- Die ACs werden in einer Tabelle verwaltet und in Batches generiert. Ein Batch wird innerhalb der Tabelle durch einen Prefix (= Verwendungszweck) und eine Batch Number identifiziert. Der Access Code besteht insgesamt aus drei Teilen: dem Batch-Prefix aus bis zu 7 Großbuchstaben, der bis zu dreistelligen Batch Number und einer zehnstelligen Zufallszahl, die innerhalb eines Batches eindeutig ist. Das Anzeigeformat ist wie folgt:
+
+ APP-1-1234567890
+
+ In der Tabelle werden zu jedem AC außerdem abgespeichert: Serial Number (innerhalb eines Batches fortlaufend), Cost (Kosten der jeweiligen Scratch Card), Date (Datum der Entwertung), Student (Id oder Registration Number des Studenten, für den der AC entwertet wurde)
+
+- Zu jedem Batch müssen das Herstellungsdatum und der Nutzer (in der entsprechenden Log-Datei) protokolliert werden. 
+
+- Bei der Herstellung eines AC-Batches muss eine für den AC-Verwalter (eigene Rolle) zugängliche csv-Datei erzeugt werden, deren Dateinamen die Nutzer-Id, das Herstellungsdatum, den Prefix und die Batchnummer enthält. Ansonsten enthält die Datei drei Datenspalten: Serial Number, AC (im Anzeigeformat) und Cost. 
+
+- Schließlich muss die Möglichkeit der Archivierung (und wahlweise Löschung) eines Batches sowie des Reimports von Archiv-Dateien gegeben sein. In den Archivdateien müssen zusätzlich die Spalen: Date und Student enthalten sein.
+
+**Such- und Editierfunktionen**
+
+- Innerhalb eines Batches müssen ACs über das Portal auffindbar sein. Suchkriterien sind: Student (Id oder Registration Number des Studenten, für den der AC entwertet wurde), PIN oder Serial Nimber. 
+
+- Gefundene ACs müssen disabled oder re-enabled werden können. Dabei wird lediglich das Student-Feld überschrieben. Beim Disablen wird der folgende Textstring eingetragen: ``- disabled by <User Name> -`` 
+
+
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/applicants.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/applicants.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/applicants.rst	(revision 6703)
@@ -0,0 +1,67 @@
+Applicants
+***********
+
+Das Applicants Package dient der Anmeldung einer großen Zahl von Personen (bis 50.000) zur Teilnahme an einem hochschulinternen Screening-Verfahren, welche die Zulassung zum Studium an der Hochschule regeln. Es gibt zur Zeit acht verschiedene Screening-Verfahren mit zum Teil mehreren unterschiedlichen Anmeldeformularen. Ein Screening wird in der Regel durch einen Massentest ('Exercise') begleitet. Manchmal spielt auch nur die Herkunft (z.B. aus dem armen Norden) oder das Geschlecht eine Rolle bei der Vergabe von Studienplätzen. Hier die verschiedenen Screening-Verfahren:
+
+**1. Universities**
+
+PUME: Post University Matriculation Examination 
+ Die UME-Daten werden von JAMB übermittelt. Diese Studenten haben schon eine JAMB-Prüfung absolviert, die sich UME nennt. JAMB ist die zentrale nigerianische Zulassungsstelle, über die die Zuweisung der Applikanten an die verschiedenen Hochschulen gesteuert wird.
+    
+PUDE: Post University Direct Entrance
+ Die UDE-Daten werden von JAMB übermittelt. Diese Studenten haben keine JAMB-Prüfung absolvieren müssen, ihre Daten werden nur weitergeleitet.
+    
+DP: Screening für Diploma Students
+
+CT: Screening für Certificate Students (einige Certificates tragen den offiziellen Namen 'Certificate'')
+    
+SANDWICH: Screening für Sanwich Programme Students (= Part-Time Degree in Education)
+    
+PT: Screening für Part-Time Degree Students (das sind ältere Studenten, die ein Abendstudium absolvieren)
+    
+(CEST: Common Entrance Screening Test) 
+ Obwohl hier aufgeführt, ist CEST **kein** eigenständiges Screeningverfahren. Es ist ein Test, der bei verschiedenen Screening-Verfahren zum Einsatz kommt. Das hat mich anfangs auch sehr verwirrt, auch dass oft nur von Screening gesprochen wird und automatisch ein Test gemeint ist. So gibt es z.B. ein Screening Venue. Ich habe aber gelernt, dass ein Screening auch ganz ohne Test auskommen kann.    
+
+**2. (Federal) Colleges of Education**
+
+PCE: (Post-) Polytechnics and Colleges of Education Matriculation Examination
+ Die PCE-Daten werden von JAMB übermittelt. Diese Studenten haben schon eine JAMB-Prüfung absolviert, die sich PCE nennt. Eigentlich müsste das Screening-Verfahren PPCE heißen, denn es ist das Pendent zu PUME. PCE-Applikanten streben als Abschluss das NCE =  Nigerian Certificate in Education an, welches drei Jahre dauert.
+    
+PRENCE: Preliminary-NCE = Screening für Vorstudium des Nigerian Certificate in Education
+ Ich habe nicht ganz verstanden, welche Studenten so ein einjähriges Vorstudium absolvieren müssen. Vermutlich ist die Schulvorbildung entscheident (s. http://www.nasarawastate.org/articles/56/1/-College-of-Education-Akwanga/Page1.html). Die Pre-NCE Absolventen machen dann automatisch mit dem NCE-Programm weiter. Im SRP starten diese Studenten im Level 000 und gehen dann automatisch in das Level 100 über, ein Trick, der sich bewährt hat.
+    
+Darüber hinaus bieten die FCEs auch noch Sandwich und Diploma-Programme an. Die Formulare der Universitäten können benutzt werden.
+
+Die Frage tauchte auf, wie wir mit Screeningverfahren umgehen, für die es mehrere Anmeldeformulare gibt (z.B. PUME und PUME 2nd Choice). Ich schlage vor, jedes Anmeldeformular als eigenständigen Container für Applicant Records zu implementieren. Das Screening interessiert uns recht wenig, nur das Formular ist für uns interessant. An dem formularspezifischen Applicants-Container hängen dann die Informationen: Start Date, End Date (= Deadline) und Title (Liste kann erweitert werden). Wir benötigen somit nur zwei Container-Ebenen, wobei die Id des Applicants-Containers sich zusammensetzen muss aus dem Kürzel des Screeningverfahrens (+ evtl. Kennung unterschiedlicher Anmeldeformulare) + Sessionkennung. Der Pfad für eine PUME-Application könnte somit wie folgt aussehen: ``applicants/pume_11/123456``. Innerhalb eines Applicants-Containers ist die Id eindeutig. Beim Export der Daten über Sessiongrenzen hinweg müsste die Kennung, in diesem Fall ``pume_11``, der Id noch vorangestellt werden. In einer CSV-Datei würde die Id des Records dann ``pume_11_123456`` lauten.
+
+Bei den Certificates gibt as das Attribut application_category. Dieses Attribut hilft bei der Gruppierung von Certificates, die den Antragstellern zur Auswahl gestellt werden. Hier werden die Screening-Typen DP, CT und PT noch einmal zusammengefasst, da sie anscheinend alle zu den gleichen Abschlüsse führen. Auch kommt bei all diesen Studenten der CEST zum Einsatz.
+
+Für den Worksflow müssen zwei Gruppen unterschieden werden. PUME, PUDE und PCE durchlaufen vorher das JAMB-Screening, d.h. JAMB liefert den Basisdatensatz, der inkl. der Passfotos über das Data Center importiert werden muss. Bei der Anmeldung im Portal müssen sich die Applikanten erst durch ihre JAMB Registration Number identifizieren. Zusätzlich geben sie noch eine Application PIN ein, mit der sie die Berechtigung gekauft haben, das Formular weiter auszufüllen zu dürfen. Alle anderen Applikanten geben nur die PIN ein. Ihr Datensatz wird in diesem Moment erzeugt. Als Registration Number wird dann (anstatt die JAMB RegNo) die Batch- und die Seriennummer der Scratch Card verwendet (Bauweise s.u.).
+
+Die Vorgehensweise und der Workflow ist wie folgt:
+
+- JAMB-Datensätze werden im Status 'created' importiert.
+
+- Applikanten öffnen die entsprechende Eingabemaske für die PIN und ggf. der JAMB Registration Number. Der PIN Prefix entspricht dem obigen Screeining-Kürzel. Nach dem ersten Zugriff auf einen Datensatz ändert sich dessen Status nach 'edited'.
+
+- Das Formular öffnet sich, das Passbild wird hochgeladen (falls noch nicht vorhanden) und die Felder werden ausgefüllt. Etwas komplizierter sind die Eingabefelder für Zeugnisnoten. Das müsste man sich mal im alten SRP anschauen.Das Passbild wird in einem separaten Verzeichnis mit der Registration Number als Name abgespeichert. Nur jpg-Bilder mit einer begrenzten  Bytegröße werden erlaubt. 
+
+- Es gibt nur ein einziges Select-Feld, das sich Daten aus der Academic Section holt: das Auswahlfeld des gewünschten Study Course (=Certificate). Die Auswahl möglicher Certificates geschieht dynamisch. Das Feld application_category der Certificates dient als Filter.
+
+- Wenn alle Pflichtfelder ausgefüllt sind, kann der Datensatz vom Applikant submitted werden. Der Status des Datensatzes ändert sich entsprechend. Nach der Einreichung erscheint ein Drucksymbol zum Ausdrucken eines Slips. Der Slip öffnet sich in einem separaten Fenster und zeigt nur eine Auswahl der Felder aus dem Datensatz an. Der Slip dient zur Vorlage bei der Vorprüfung an der Universität.
+
+- Nach der Massenprüfung an der Universität werden die Ergebnisse der Prüfung importiert und der Datensatz entweder auf 'non admitted' oder 'admitted' gesetzt. In den admitted Datensätzen wird zudem der certificate code importiert, des Kurses für den die Applikanten vorläufig zugelassen wurden. 
+
+- Ein paar Tage später werden die admitted Datensätze in das Portal kopiert (ca. 5000 bis 7000 Stück), d.h. in Studentenobjekte umgewandelt. Die Applicants-Tabelle wird nach einem Jahr geleert. Sehr bürokratisch ist die Verwaltung der Registration Number. CEST und Sandwich Applikanten bekommen eine eigene Registration Number verpasst (siehe WAeUPTool.py)::
+
+    if brain.screening_type in ('dp','pt','ct','cest','sandwich','sandwich2008') and brain.serial and brain.entry_session:
+        if brain.course1:
+            reg_no = "%s%s/%s" % (brain.course1[:3],brain.serial,brain.entry_session)
+        else:
+            reg_no = "%s%s/%s" % (brain.course_admitted[:3],brain.serial,brain.entry_session)
+    elif brain.screening_type in ('pume2','pde2'):  #second choice Uniben applicants
+        reg_no = brain.jamb_reg_no.upper()
+    else:
+        reg_no = brain.reg_no
+
+ Deswegen ist zu überlegen, ob wir nicht grundsätztlich zwei Registration Numbers verwalten: Einmal die portaleigene, die sich aus der Session, des Screening-Typen und einer fortlaufenden Nummer zusammensetzt und die den Datensatz identifiziert, und zum anderen die vorgegebene aus Nigeria, die auf dem Slip erscheint und bei der Datenverarbeitung keine weitere Rolle mehr spielt. Nur beim Import oder beim Transformieren der Daten muss aufgepasst werden, dass eine Registration Number  nicht zweimal vergeben werden darf, sie muss eindeutig sein. Studenten müssen hinterher über ihre Registration Number und Matriculation Number eindeutig identifizierbar sein. Das ist erfahrungsgemäß nicht immer der Fall. SIRP kann der Hochschule behilflich sein, Mehrfachzuordnungen zu identifizieren.
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/datacenter.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/datacenter.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/datacenter.rst	(revision 6703)
@@ -0,0 +1,27 @@
+Data Center
+-----------
+
+Über das Data Center (ehemals Upload Section) werden alle hochgeladenen Dateien verwaltet und importiert. Folgende Eigenschaften haben sich im SRP bewährt:  
+
+- Speicherung des Datums und der User Id beim Hochladen einer Datei
+
+- Manuelle Auswahl des Importers anbieten
+
+ Unabhängig vom Header der hochgeladenen Datei sollten alle Importer zur Auswahl angeboten werden. Der ausgewählte Importer sollte gespeichert werden.
+
+- CSV-Headerprüfung durchführen
+
+ Der Header muss nach der Auswahl eines Importers mit dem Import-Schema verglichen werden. Bei fehlerhaftem Header sollte eine Fehlerbeschreibung und eine Online Editiermöglichkeit des File Headers angeboten werden (siehe SRP).
+
+- Import modes 'edit', 'create' and 'remove' etablieren
+
+ Jedem Datensatz sollte über den import_mode mitgeteilt werden, ob er vorhandene Datensätze überschreibt, ob er als neuer Datensatz hinzugefügt oder ob er per import die entsprechenden Objekte gelöscht und archiviert werden sollen. Im create und im edit mode sind unterschiedliche Felder required.
+
+- Import der Daten
+
+ Der Import bleibt zunächst dem Portaladministrator vorenthalten. Datum und User Id sollten beim Import abgespeichert werden.
+
+- Pending Data Files etablieren
+
+ Auf eine automatisierte Vorab-Datenprüfung kann zunächst verzichtet werden. Nicht importierbare Datensätze müssen mit einer Fehlermeldung in einer Pending Datei landen, die manuell nachimportiert werden können muss. 
+
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/index.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/index.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/index.rst	(revision 6703)
@@ -0,0 +1,17 @@
+Instructions for Developers (German)
+************************************
+
+by Henrik Bettermann
+
+
+This German introduction describes the properties and workflows of the existing Student Registration Portal (SRP) which has been developed by Joachim Schmitz (†) and Henrik Bettermann in close cooperation with the Central Registration and Processing Unit (CRPU) of the University of Benin in Nigeria. The introduction is has been written for Uli Fouquet, the senior software engineer of waeup.sirp.
+
+Diese deutschsprachige Anleitung beschreibt im Wesentlichen die Eigenschaften und Abläufe des existierenden Student Registration Portals (SRP), welches von Joachim Schmitz (†) und Henrik Bettermann in enger Zusammenarbeit mit der Central Registration and Processing Unit (CRPU) der University of Benin in Nigeria entwickelt wurde. Die Anleitung wurde in erster Linie für Uli Fouquet geschrieben, den Chefentwickler von waeup.sirp.
+
+.. toctree::
+
+   datacenter
+   accesscodes
+   applicants
+   users
+   students
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/students.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/students.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/students.rst	(revision 6703)
@@ -0,0 +1,40 @@
+Students
+********
+
+Nach der Application erfolgt die Registrierung der Studenten. Studentendaten werden entweder durch Übernahme der Application-Daten, durch Import bestehender Datensätze oder durch Hinzufügen eines leeren Datensatzes erzeugt. An dieser Stelle sei zunächst nur die Übernahme der Applicationdaten beschrieben.
+
+**1. Aus Applicants werden Studenten**
+
+Nachdem durch Datenimport Applicants in den Status 'admitted' versetzt worden sind und gleichzeitig ein course_admitted zugeordnet wurde, werden alle Applicants im Status 'admitted' in Studentendatensätze umgewandelt und gleichzeitig in den Status 'created' versetzt.
+
+Dabei wird zunächst ein Student-Objekt (Container?!) erzeugt und es werden Basisdaten von dem Applicant-Objekt in das neu erzeugte Student-Objekt kopiert (inkl. Passbild). Beim Erzeugen des Student-Objektes wird eine Student Id generiert die zusätzlich in das originale Applicant-Objekt eingetragen wird.
+
+Sinnvoll wäre es, das originale Applicant-Objekt (ohne Passbild) in den Student-Container zu kopieren. Die meisten Daten aus dem Application-Objekt werden zwar nie wieder gebraucht, trotzdem ist es sinnvoll, die Application-Daten von zugelassenen Studenten vollständig zu erhalten.
+
+Die Datenübernahme in die Students Section ist im Prinzip noch Aufgabe des (eigenständigen) Applicationmoduls. Das Studentmodul ist Basisausstattung von SIRP und sollte auch ohne Applicationmodul lauffähig sein.
+
+**2. First-time Login (Admission Checking)**
+
+Studenten loggen sich das erste Mal mit ihrer Student Id und einem neuen Access Code (PIN) ein. Der AC wird dann zum Password. Falls die Student Id noch nicht bekannt ist, z.B. beim Umgehen der Application, muss eine (z.B. JAMB) Registration Number als Ersatz dienen.
+
+Jetzt prüft der Student zunächst seine Zulassungsdaten und ob der ihm zugewiesene Studiengang korrekt und genehm ist. Im SRP bestand die Möglichkeit, Einspruch zu erheben. Diese Möglichkeit hat sich nicht bewährt und nur zu Verwirrungen geführt. Einspruch muss außerhalb des Portals eingelegt werden
+können. Sind die Daten korrekt, kann der Clearance-Prozess beginnen.
+
+**3. Clearance**
+
+Die Clearence wird mit einem Clearance AC eingeleitet. Eine spezielle Clearance Form muss ausgefüllt werden und viele Scans hochgeladen werden. Ist dies erfolgt, wird die Form an den Clearance Officer des Departments oder der Fakultät (User mit entsprechender lokaler Rolle) submitted. Der Clearance Officer überprüft die Daten. Sind die Daten korrekt, wird die Transition 'validate_and_clear' ausgeführt. Bei fehlerhaften Daten wird die Transition 'reject_clearance' ausgeführt und der Student erhält die Möglichkeit seine Daten zu korrigieren und erneut einzureichen. 'reject_clearance' wird durch Versand einer Email dem Studenten mitgeteilt. Nach erfolgreicher Clearance werden die Studiengebühren für das erste Jahr bezahlt (siehe Abschnitt 'Bezahlung der Studiengebühren').
+
+**4. Personal Data Form**
+
+Von Beginn an ist ein Teil der Daten, die nicht für die Clearance zwingend erforderlich und entscheidend sind, in der Personal Data Form jederzeit zum Editieren offen. Die Studentendaten sind somit dreigeteilt: 1. unveränderliche Application Data, 2. Clearance Data, die nach der Clearance gesperrt werden und 3. allzeit offene und veränderbare Personal Data.
+
+**5. Bezahlung der Studiengebühren**
+
+Die Bezahlung der Studiengebühren löst die Transition pay_school_fee aus. Studiengebühren könnten bei einer übersichtlichen Staffelung der Gebühren über die Eingabe eines AC bezahlt werden, d.h. für jede Gebührenform gäbe es einen eigenen AC-Batch. In Nigeria gibt es diese einfache Staffelung nicht. Komplexe Algorithmen oder aufwendige Tabellen kommen zur Ermittlung der Studiengebühren zum Einsatz. Hinzu kommen Zusatzgebühren für die verschiedenen Einrichtungen, so dass nur eine dynamische Berechnung der Gebühren und die Bezahlung über ein externes Paymentportal in Frage kommt. Dem Paymentportal wird von SIRP nicht nur die Höhe der Gebühren mitgeteilt, sondern auch wie sich die Gebühren aufteilen und auf welche Konten die Gebühren überwiesen werden müssen.
+
+Aufgrund der Komplexität der Bezahlung ist es sinnvoll, ein eigenes Modul für die Online-Bezahlung von Gebühren zu entwickeln. Dieses Modul erzeugt Payment-Objekte die bei erfolgreicher Bezahlung als Quittung dienen und die im Student-Container abgelegt werden. Die Transition wird durch die Online-Bezahlung nicht automatisch vorgenommen, sondern muss - im Gegensatz zum SRP - manuell ausgelöst werden. Im gesonderten Payment-Modul werden Payment-Objekte erzeugt, die durch den Callback des Paymentgateways (das kann in der Zukunft unter Umständen auch ein Kreditkartenunternehmen sein) als bezahlt markiert werden. Dies ermöglicht, eine Bezahlung auch vorab oder nachträglich vorzunehmen, d.h. auch dann, wenn es der Workflow des Studenten gar nicht erlaubt. Zum manuellen Auslösen der Transition müsste nach einem Paymentobjekt gesucht werden, dass die erforderlichen Kriterien erfüllt. Im Falle der Studiengebühr wären das die die Payment Category, die Student Id, das Studienfach und die Session. vermutlich ist ein Catalog sinnvoll.
+
+**6. Kursregistrierung**
+
+- in Bearbeitung -
+
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/users.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/users.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/german/users.rst	(revision 6703)
@@ -0,0 +1,60 @@
+User Management
+***************
+
+Das User- und Rollen-Management in im SRP lehnt sich an die Nutzer- und Rechteverwaltung des Nuxeo CPS an und wird im Wesentlichen über die Gruppenzugehörigkeit von Nutzern gesteuert. D.h. es werden wenige Rollen definiert, die einer ganzen Gruppe lokal zugewiesen werden.
+
+- Rollen
+
+    SectionReader und SectionManager
+      Diese beiden Rollen wurden vom CPS übernommen. Es wurde auch noch die SectionOfficer-Rolle eingeführt, die aber mit der SectionManager-Rolle identisch und somit eigentlich überflüssig ist. 
+
+    Student
+      Die Studentenrolle findet nur noch in logged_in.py Verwendung. Ein Student hat als Owner seiner Objekte gewisse Rechte und hat über die Zuweisung
+      
+      ::
+          
+        academics.manage_setLocalGroupRoles(groupid='role: Authenticated',roles=('SectionReader',)) 
+            
+      in exportimport.py des SRP die SectionReader-Rolle für die Academic Section. 
+      
+    ClearanceOfficer und CourseAdviser
+      Die ClearanceOfficer-Rolle wird in der Academic Section auf Faculty-Ebene lokal vergeben, die CourseAdviser-Rolle auf StudyLevel-Ebene. Die Mitglieder dieser Gruppen bekommen damit **keine** weiteren Rechte in der Academic Section. Jedoch werde über einen recht umständlichen Algoritmus in PatchCPSUserFolderUserFolder.py Rollen in die Students Section übertragen.  Hierzu dienen die Skripte isClearanceOfficer.py und isCourseAdviser.py. Clearance Officer dürfen in bestimmten Registration Status Transitions ausführen, Course Advisers dürfen Kurslisten validieren.  
+
+
+- Gruppen
+
+    CampusOfficers
+      sind Section Manager für das Campus-Objekt.
+    
+    AcademicsOfficers
+      sind Section Manager für die Academics Section.
+    
+    StudentsOfficers
+      sind Section Manager für die Students Section.
+    
+    StudentsReaders
+      sind Section Reader für für die Students Section.
+    
+    MemberAdmins
+      sind Section Manager der Student Section sowie für site.portal_directories.members, d.h. sie haben die Berechtigung, neue User hinzuzufügen bzw. zu löschen.
+    
+    UploadOfficers
+      sind Section Manager für die Uploads Section.
+      
+    AccessCodeOfficers
+      sind Section Manager für die PIN Section.
+    
+    AccommodationOfficers
+      sind Section Manager für die Accommodation Section.
+    
+    ClearanceOfficers und CourseAdvisers
+      sind Gruppen, die für die lokale Rollenzuweisung und das Patch (s.o.) benötigt werden. Im SRP **müssen** diese Gruppen zugewiesen werden. Die Vergabe der lokalen Rolle reicht nicht aus.   
+    
+    
+- Password Policy
+
+  Im SRP werden neue Staff-Mitglieder von einem User der Gruppe MemberAdmins eingetragen. Dieser ist letztendlich auch verantwortlich für sichere Passwörter. Es gibt keine Restriktionen bei der Passworteingabe. Zudem werden Passwörter unverschlüsslet abgespeichert. Mit entsprechenden Exportfunktionen kann mann eine csv-Tabelle aller User inkl. ihrer unverschlüsselten Passwörter exportieren. All diese Funktionen müssen überdacht und verbessert werden.
+
+- Anbindung an Plone und Moodle
+
+  Die elegante Lösung wäre ein Single Sign-on Verfahren, bei dem sich alle Portale die Credentials von einem LDAP-Server holen. Alternativ sollte man über eine Authentifizierung mithilfe der Daten aus SIRP nachdenken. Vorteil wäre, dass man SIRP alleine betreiben kann, ohne zusätzliche Dienste auf dem Server zu installieren. 
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/index.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/index.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/developerdocs/index.rst	(revision 6703)
@@ -0,0 +1,9 @@
+Developer Documentation
+***********************
+
+.. toctree::
+   :maxdepth: 2
+   
+   developernotes
+   api/index
+   german/index
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/documentation.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/documentation.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/documentation.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/glossary.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/glossary.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/glossary.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/index.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/index.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/index.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/intro.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/intro.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/intro.rst	(revision 6703)
@@ -0,0 +1,1 @@
+.. include:: ../../README.txt
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/license.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/license.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/license.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/userdocs/buildingdocs.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/userdocs/buildingdocs.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/userdocs/buildingdocs.rst	(revision 6703)
@@ -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/uli-studentpw/docs/source/userdocs/index.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/userdocs/index.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/userdocs/index.rst	(revision 6703)
@@ -0,0 +1,9 @@
+User Documentation
+******************
+
+.. toctree::
+   :maxdepth: 2
+
+   install
+   install_on_windows
+   buildingdocs
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/userdocs/install.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/userdocs/install.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/userdocs/install.rst	(revision 6703)
@@ -0,0 +1,3 @@
+.. _installing_linux:
+
+.. include:: ../../INSTALL.txt
Index: /main/waeup.sirp/branches/uli-studentpw/docs/source/userdocs/install_on_windows.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/docs/source/userdocs/install_on_windows.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/docs/source/userdocs/install_on_windows.rst	(revision 6703)
@@ -0,0 +1,3 @@
+.. _installing_win:
+
+.. include:: ../../INSTALLWIN.txt
Index: /main/waeup.sirp/branches/uli-studentpw/etc/README.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/etc/README.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/etc/README.txt	(revision 6703)
@@ -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/uli-studentpw/etc/debug.ini.in
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/etc/debug.ini.in	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/etc/debug.ini.in	(revision 6703)
@@ -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/uli-studentpw/etc/deploy.ini.in
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/etc/deploy.ini.in	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/etc/deploy.ini.in	(revision 6703)
@@ -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/uli-studentpw/etc/site.zcml.in
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/etc/site.zcml.in	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/etc/site.zcml.in	(revision 6703)
@@ -0,0 +1,42 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           i18n_domain="waeup.sirp">
+
+  <include package="waeup.sirp" />
+
+    <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" />
+
+      <role id="zope.Manager" title="Site Manager" />
+      <role id="zope.Member" title="Site Member" />
+      <grantAll role="zope.Manager" />
+      <grant role="zope.Manager"
+             principal="zope.manager" />
+   </configure>
+</configure>
Index: /main/waeup.sirp/branches/uli-studentpw/etc/zdaemon.conf.in
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/etc/zdaemon.conf.in	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/etc/zdaemon.conf.in	(revision 6703)
@@ -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/uli-studentpw/etc/zope.conf.in
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/etc/zope.conf.in	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/etc/zope.conf.in	(revision 6703)
@@ -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/uli-studentpw/setup.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/setup.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/setup.py	(revision 6703)
@@ -0,0 +1,114 @@
+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.file',
+    'zope.interface >= 3.6.0',
+    'zope.testbrowser',        # XXX: test_permissions needs this
+    'zope.i18n',
+    'zope.mimetype',
+    'zope.errorview',
+    'zope.schema >= 3.8.0',
+    ],
+
+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', 'README.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,
+        ),
+      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/uli-studentpw/src/waeup/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/__init__.py	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/README.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/README.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/README.txt	(revision 6703)
@@ -0,0 +1,130 @@
+The waeup.sirp package
+**********************
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
+
+A portal software for student registration.
+
+Universities
+============
+
+``University`` objects are the base of all functionality provided by
+this package.  They contain all facilities of a university.
+
+We can easily create universities::
+
+  >>> from waeup.sirp.app import University
+  >>> u = University()
+  >>> u
+  <waeup.sirp.app.University object at 0x...>
+
+Universities have a name.
+
+  >>> u.name
+  u'Sample University'
+
+Universities are basically also containers for faculties, students and
+hostels::
+
+  >>> u['faculties']
+  <waeup.sirp.university.facultycontainer.FacultyContainer object at 0x...>
+
+  >>> u['students']
+  <waeup.sirp.students.container.StudentsContainer object at 0x...>
+
+We can export universities. For this we lookup an appropriate exporter
+first::
+
+  >>> from waeup.sirp.interfaces import IWAeUPExporter
+  >>> exporter = IWAeUPExporter(u)
+  >>> 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 IWAeUPXMLExporter
+  >>> exporter = IWAeUPXMLExporter(u)
+  >>> f = exporter.export()
+  >>> f
+  <cStringIO.StringO object at 0x...>
+
+  >>> print f.read()
+  <?xml version="1.0" encoding="utf-8" ?>
+  <pickle>
+    <initialized_object id="...">
+  ...
+  </pickle>
+
+
+Faculties
+=========
+
+Faculties are containers for departments. They are intended to be
+managed by universities.
+
+We can create faculties easily::
+
+  >>> from waeup.sirp.university.faculty import Faculty
+  >>> f = Faculty()
+  >>> f
+  <waeup.sirp.university.faculty.Faculty object at 0x...>
+
+Also faculties want to be named::
+
+  >>> f.title
+  u'Unnamed Faculty'
+
+Departments
+===========
+
+
+WAeUP SIRP plugins
+==================
+
+waeup.sirp provides an API to 'plugin' components. Things that should
+be setup at creation time of a WAeUP SIRP application can indicate
+that by providing a utility providing IWAeUPSIRPPlugin.
+
+The plugins are looked up by an created app, which then will call the
+``setup()`` method of each plugin.
+
+   >>> from waeup.sirp.interfaces import IWAeUPSIRPPluggable
+   >>> from zope.component import getAdapters, getUtilitiesFor
+   >>> sorted(list(getUtilitiesFor(IWAeUPSIRPPluggable)))
+   [(u'accesscodes', <waeup.sirp.accesscodes...AccessCodePlugin ...)]
+
+
+We can provide a new plugin like this:
+
+   >>> import grok
+   >>> from waeup.sirp.interfaces import IWAeUPSIRPPluggable
+   >>> class MyPlugin(grok.GlobalUtility):
+   ...   grok.implements(IWAeUPSIRPPluggable)
+   ...   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 WAeUP 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/uli-studentpw/src/waeup/sirp/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/__init__.py	(revision 6703)
@@ -0,0 +1,13 @@
+import grok
+from waeup.sirp.interfaces import IWAeUPObject
+from zope.annotation.attribute import AttributeAnnotations
+from zope.annotation.interfaces import IAnnotations
+
+class WAeUPAttributeAnnotations(AttributeAnnotations, grok.Adapter):
+    """An adapter to IAnnotations for any WAeUPObject.
+
+    Providing this adapter, each WAeUP object becomes (attribute)
+    annotatable.
+    """
+    grok.provides(IAnnotations)
+    grok.context(IWAeUPObject)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/__init__.py	(revision 6703)
@@ -0,0 +1,31 @@
+"""
+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, AccessCode, AccessCodeBatch, AccessCodeBatchContainer)
+
+# Public API of this submodule
+__all__ = [
+    'AccessCodeBatchContainer',
+    'AccessCodeBatch',
+    'AccessCode',
+    'get_access_code',
+    'invalidate_accesscode',
+    'disable_accesscode',
+    'reenable_accesscode',
+    ]
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/accesscode.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/accesscode.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/accesscode.py	(revision 6703)
@@ -0,0 +1,520 @@
+"""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 IWAeUPSIRPPluggable, IObjectHistory
+from waeup.sirp.accesscodes.interfaces import (
+    IAccessCode, IAccessCodeBatch, IAccessCodeBatchContainer
+    )
+from waeup.sirp.accesscodes.workflow import DISABLED, USED
+
+class ManageACBatches(grok.Permission):
+    grok.name('waeup.manageACBatches')
+
+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
+        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 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):
+        """Add an access-code.
+        """
+        ac = AccessCode(num, pin)
+        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):
+    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.
+        """
+        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
+
+class AccessCodePlugin(grok.GlobalUtility):
+    grok.name('accesscodes')
+    grok.implements(IWAeUPSIRPPluggable)
+
+    def setup(self, site, name, logger):
+        site['accesscodes'] = AccessCodeBatchContainer()
+        logger.info('Installed container for 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):
+    """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.
+
+    :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')
+    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):
+    """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:
+        fire_transition(access_code, 'use', comment=comment)
+        return True
+    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)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/accesscode.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/accesscode.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/accesscode.txt	(revision 6703)
@@ -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.WAeUPSIRPUnitTestLayer
+
+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 WAeUP SIRP university
+  instances so that they provide support for access-codes.
+
+  .. attribute:: grok.implements(IWAeUPSIRPPluggable)
+  .. 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
+  IWAeUPSIRPPluggable 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 IWAeUPSIRPPluggable
+    >>> plugin = getUtility(IWAeUPSIRPPluggable, 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/uli-studentpw/src/waeup/sirp/accesscodes/api.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/api.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/api.txt	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/accesscodes/browser.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/browser.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/browser.py	(revision 6703)
@@ -0,0 +1,217 @@
+"""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 WAeUPPage, WAeUPAddFormPage
+from waeup.sirp.browser.breadcrumbs import Breadcrumb
+from waeup.sirp.browser.viewlets import (
+    AdminTask, AddActionButton, SearchActionButton, BatchOpButton, ManageLink)
+from waeup.sirp.interfaces import IWAeUPObject
+
+from waeup.sirp.accesscodes.interfaces import (
+    IAccessCodeBatchContainer, IAccessCodeBatch,
+    )
+from waeup.sirp.accesscodes.catalog import search
+
+grok.context(IWAeUPObject)
+
+class BatchContainerPage(WAeUPPage):
+    grok.name('index')
+    grok.context(IAccessCodeBatchContainer)
+    grok.template('batchcontainer')
+    grok.require('waeup.manageACBatches')
+
+    title = '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]
+        for name in batches:
+            batch = self.context[name]
+            csv_file = batch.archive()
+            self.flash('Archived %s (%s)' % (name, csv_file))
+            if delete is None:
+                continue
+            del self.context[name]
+            self.flash('Deleted batch %s' % name)
+
+class AddBatchPage(WAeUPAddFormPage):
+    grok.name('add')
+    grok.context(IAccessCodeBatchContainer)
+    grok.require('waeup.manageACBatches')
+
+    title = 'Create Access Code Batch'
+    pnav = 0
+
+    form_fields = grok.AutoFields(IAccessCodeBatch).select(
+        'prefix', 'entry_num', 'cost')
+
+    @grok.action('Cancel')
+    def cancel(self, *args, **kw):
+        self.flash('Batch creation cancelled.')
+        self.redirect(self.url(self.context))
+
+    @grok.action('Create batch')
+    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 (%s entries)' % data['entry_num'])
+        self.flash('Data written to %s' % csv_file)
+        self.redirect(self.url(self.context))
+
+class ReimportBatchPage(WAeUPPage):
+    """Screen for reimporting AC batches.
+    """
+    grok.name('reimport')
+    grok.context(IAccessCodeBatchContainer)
+    grok.template('reimportbatchpage')
+    grok.require('waeup.manageACBatches')
+
+    title = '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: %s' % filename)
+                continue
+            self.flash('Successfully reimported: %s' % filename)
+        self.redirect(self.url(self.context))
+
+class BatchContainerSearchPage(WAeUPPage):
+    grok.name('search')
+    grok.context(IAccessCodeBatchContainer)
+    grok.template('searchpage')
+    grok.require('waeup.manageACBatches')
+
+    title = 'Access Codes'
+    pnav = 0
+    label = 'Search and manage access codes'
+
+    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
+        #import pdb; pdb.set_trace()
+        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]
+        for entry in entries:
+            if 'disable' in form:
+                try:
+                    comment = u"AC disabled"
+                    self.context.disable(entry, comment)
+                    self.flash('%s disabled.' % entry)
+                except InvalidTransitionError:
+                    self.flash('%s: Disable transition not allowed.' % entry)
+            elif 'enable' in form:
+                try:
+                    comment = u"AC re-enabled"
+                    self.context.enable(entry, comment)
+                    self.flash('%s (re-)enabled.' % entry)
+                except InvalidTransitionError:
+                    self.flash('%s: Re-enable transition not allowed.' % 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/uli-studentpw/src/waeup/sirp/accesscodes/browser.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/browser.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/browser.txt	(revision 6703)
@@ -0,0 +1,436 @@
+: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 WAeUP 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 naturally
+no batches available:
+
+    >>> browser.open('http://localhost/myuniversity')
+    >>> browser.getLink('Access Codes').click()
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    <h2>Access Code Batches</h2>
+    ...
+    ... The following batches are available:
+    ...No batches yet...
+    ...
+
+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"
+    ...
+    <h2>Access Code Batches</h2>
+    ...
+    ... 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"
+    ...
+    <li ...>Archived APP-2 (APP-2_archive-...-zope.mgr.csv)</li>
+    ...
+
+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"
+    ...
+    <li ...>No batch selected.</li>
+    ...
+
+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>... - AC initialized by zope.mgr</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>... - AC initialized by zope.mgr</td>
+    ...
+
+And we can search for text in history messages of access
+codes. Looking for the string ``AC 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 = 'AC 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>... - AC initialized by zope.mgr</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 = 'AC 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 ...
+    <li class="message">APP-1-<10-DIGITS> disabled.</li>
+    ...
+
+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 ...
+    <li ...>APP-1-<10-DIGITS>: Disable transition not allowed.</li>
+    ...
+
+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 ...
+    <li class="message">APP-1-<10-DIGITS> (re-)enabled.</li>
+    ...
+
+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 ...
+    <li ...>APP-1-<10-DIGITS>: Re-enable transition not allowed.</li>
+    ...
+
+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/uli-studentpw/src/waeup/sirp/accesscodes/browser_templates/admintaskacbatches.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/browser_templates/admintaskacbatches.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/browser_templates/admintaskacbatches.pt	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/accesscodes/browser_templates/batchcontainer.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/browser_templates/batchcontainer.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/browser_templates/batchcontainer.pt	(revision 6703)
@@ -0,0 +1,44 @@
+<h2>Access Code Batches</h2>
+
+<form method="POST">
+  <p>
+    The following batches are available:
+  </p>
+
+  <table>
+    <thead>
+      <tr>
+        <th>&nbsp;</th>
+	<th>Prefix</th><th>Entries/(invalidated)</th><th>Cost</th>
+	<th>Created</th><th>Creator</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="batch context/values"
+	  tal:attributes="class python: repeat['batch'].odd() and 'even' or 'odd'">
+	<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.ctime()">2009/12/24</td>
+	<td tal:content="batch/creator">some user</td>
+      </tr>
+      <tr tal:condition="not: context/values">
+	<td colspan="5"><b>No batches yet</b></td>
+      </tr>
+    </tbody>
+  </table>
+  <input type="submit" name="archive" value="Archive" />
+  <input type="submit" name="delete" value="Archive and Delete" />
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/browser_templates/reimportbatchpage.pt	(revision 6703)
@@ -0,0 +1,31 @@
+<h3>Reimport Access Code Batches</h3>
+<p>
+  Please select the files to reimport. Please note, that batches with
+  same name have to be deleted before.
+</p>
+<form method="POST">
+  <table>
+    <thead>
+      <tr>
+        <th>&nbsp;</th>
+	<th>File</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="filename context/getImportFiles"
+	  tal:attributes="class python: repeat['filename'].odd() and 'even' or 'odd'">
+	<td>
+	  <input type="checkbox" name="filenames" value="filename"
+		 tal:attributes="value filename" />
+	</td>
+	<td tal:content="filename">file.name</td>
+      </tr>
+      <tr tal:condition="python: len(list(context.getImportFiles())) == 0">
+	<td colspan="5"><b>No import batches available</b></td>
+      </tr>
+    </tbody>
+  </table>
+
+  <input type="submit" name="cancel" value="Cancel" />
+  <input type="submit" name="reimport" value="Reimport" />
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/browser_templates/searchpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/browser_templates/searchpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/browser_templates/searchpage.pt	(revision 6703)
@@ -0,0 +1,49 @@
+<form method="POST">
+
+<br />
+
+  <input type="submit" name="search" value="Search" />
+  for access codes
+
+  <select name="searchtype">
+    <option value="code">with PIN</option>
+    <option value="batch_serial">with serial</option>
+    <option 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">Serial</th><th>AC</th><th>State</th><th>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/state">unused</td>
+          <td tal:content="structure python:item.history.replace('||','<br />')">
+            history
+          </td>
+    	</tr>
+      </tbody>
+    </table>
+    <div class="actionButtons">
+        <input type="submit" name="disable" value="Disable ACs" />
+        <input type="submit" name="enable" value="Enable ACs" />
+    </div>
+
+  </div>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/catalog.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/catalog.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/catalog.py	(revision 6703)
@@ -0,0 +1,53 @@
+"""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.state = context.state
+        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':
+        results = Query().searchResults(
+            Eq(('accesscodes_catalog', searchtype), int(query)))
+    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/uli-studentpw/src/waeup/sirp/accesscodes/interfaces.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/interfaces.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/interfaces.py	(revision 6703)
@@ -0,0 +1,79 @@
+"""Interfaces of access code related components.
+"""
+from zope import schema
+from zope.interface import Interface
+from waeup.sirp.interfaces import IWAeUPObject
+
+class IAccessCode(IWAeUPObject):
+    """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',
+        )
+    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 = 1,
+        )
+    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(IWAeUPObject):
+    """A container for access code batches.
+    """
+
+    def addBatch(batch):
+        """Add a batch.
+        """
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/tests/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/tests/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/tests/__init__.py	(revision 6703)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/tests/sample_import.csv
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/tests/sample_import.csv	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/tests/sample_import.csv	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/accesscodes/tests/test_accesscode.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/tests/test_accesscode.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/tests/test_accesscode.py	(revision 6703)
@@ -0,0 +1,473 @@
+##
+## test_accesscode.py
+## Login : <uli@pu.smp.net>
+## Started on  Sun Jun 12 13:07:58 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
+##
+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.app.testing.functional import (
+    FunctionalTestSetup, getRootFolder)
+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, IWAeUPSIRPPluggable
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+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('AC 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(
+            '^....-..-.. ..:..:.. - AC 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_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(IWAeUPSIRPPluggable, plugin)
+        assert verifyClass(IWAeUPSIRPPluggable, 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 setUp(test):
+    FunctionalTestSetup().setUp()
+
+def tearDown(self, test=None):
+    FunctionalTestSetup().tearDown()
+
+
+
+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/uli-studentpw/src/waeup/sirp/accesscodes/tests/test_catalog.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/tests/test_catalog.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/tests/test_catalog.py	(revision 6703)
@@ -0,0 +1,236 @@
+##
+## test_catalog.py
+## Login : <uli@pu.smp.net>
+## Started on  Mon Jun 20 18:39:12 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
+##
+import shutil
+import tempfile
+import unittest
+from zope.app.testing.functional import (
+    FunctionalTestCase, FunctionalTestSetup, getRootFolder)
+from zope.catalog.interfaces import ICatalog
+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.interfaces import IQueryResultItem
+from waeup.sirp.testing import FunctionalLayer
+from waeup.sirp.accesscodes.accesscode import (
+    AccessCode, 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/uli-studentpw/src/waeup/sirp/accesscodes/tests/test_workflow.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/tests/test_workflow.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/tests/test_workflow.py	(revision 6703)
@@ -0,0 +1,30 @@
+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/uli-studentpw/src/waeup/sirp/accesscodes/workflow.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/workflow.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/accesscodes/workflow.py	(revision 6703)
@@ -0,0 +1,137 @@
+##
+## workflow.py
+## Login : <uli@pu.smp.net>
+## Started on  Sat Jun 11 00:35:16 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public 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 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, IWAeUPWorkflowInfo
+from waeup.sirp.workflow import WAeUPWorkflow, WAeUPWorkflowInfo
+
+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 = WAeUPWorkflow(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(WAeUPWorkflowInfo, grok.Adapter):
+    grok.context(IAccessCode)
+    grok.provides(IWAeUPWorkflowInfo)
+
+    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 = '%s' % event.comment
+    else:
+        msg = 'AC %s' % event.destination
+
+    history = IObjectHistory(obj)
+    history.addMessage(msg)
+    return
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/app.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/app.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/app.py	(revision 6703)
@@ -0,0 +1,77 @@
+import grok
+from hurry.file.interfaces import IFileRetrieval
+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.imagestorage import ImageStorageFileRetrieval, ImageStorage
+from waeup.sirp.interfaces import (
+    IUniversity, IWAeUPSIRPPluggable, IObjectUpgradeEvent, )
+from waeup.sirp.users import UserContainer
+from waeup.sirp.utils.logger import Logger
+from waeup.sirp.utils.helpers import attrs_to_fields
+
+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,)
+
+    grok.local_utility(
+        ImageStorageFileRetrieval, provides = IFileRetrieval)
+
+    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'] = UserContainer()
+        self['datacenter'] = DataCenter()
+        self['images'] = ImageStorage()
+        self['students'] = StudentsContainer()
+        self._createPlugins()
+
+    def _createPlugins(self):
+        """Create instances of all plugins defined somewhere.
+        """
+        for name, plugin in getUtilitiesFor(IWAeUPSIRPPluggable):
+            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(IWAeUPSIRPPluggable):
+            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_add(app, event):
+    app.logger.info('University `%s` added.' % getattr(app, '__name__', None))
+    return
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/app.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/app.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/app.txt	(revision 6703)
@@ -0,0 +1,61 @@
+:mod:`waeup.sirp.app` -- central components for a WAeUP portal
+**************************************************************
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
+
+.. module:: waeup.sirp.app
+
+.. class:: University
+
+  The main WAeUP 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.IFacultyContainer`.
+
+
+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.name
+    u'Sample University'
+
+    >>> myuniversity['faculties']
+    <waeup.sirp.university.facultycontainer.FacultyContainer object at 0x...>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/__init__.py	(revision 6703)
@@ -0,0 +1,21 @@
+"""This package contains everything regarding university applicants.
+"""
+# Make this a package.
+from waeup.sirp.applicants.applicant import (
+    ResultEntry, Applicant, ApplicantFactory, ApplicantTraverser,
+    )
+from waeup.sirp.applicants.container import ApplicantsContainer
+from waeup.sirp.applicants.root import (
+    ApplicantsRoot, get_applicant_data, application_exists,
+    )
+
+__all__ = [
+    'ResultEntry',
+    'Applicant',
+    'ApplicantFactory',
+    'ApplicantsContainer',
+    'ApplicantsRoot',
+    'ApplicantTraverser',
+    'application_exists',
+    'get_applicant_data',
+    ]
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/applicant.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/applicant.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/applicant.py	(revision 6703)
@@ -0,0 +1,122 @@
+##
+## applicants.py
+## Login : <uli@pu.smp.net>
+## Started on  Fri Jul 16 11:46:55 2010 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2010 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public 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 grok import index
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from waeup.sirp.interfaces import IObjectHistory
+from waeup.sirp.app import University
+from waeup.sirp.applicants.interfaces import (
+    IResultEntry, IApplicant, IApplicantEdit, default_passport_image,
+    )
+from waeup.sirp.utils.helpers import attrs_to_fields
+
+class ResultEntry(grok.Context):
+    grok.implements(IResultEntry)
+
+    def __init__(self, subject=None, score=None):
+        self.subject = subject
+        self.score = score
+
+class Applicant(grok.Model):
+    grok.implements(IApplicant,IApplicantEdit)
+    grok.provides(IApplicant)
+
+    def __init__(self):
+        super(Applicant, self).__init__()
+        IWorkflowInfo(self).fireTransition('init')
+        self.application_date = None
+        return
+
+    #def getApplicantsRootLogger(self):
+    #    return grok.getSite()['applicants'].logger
+
+    def loggerInfo(self, ob_class, comment=None):
+        target = self.__name__
+        return grok.getSite()['applicants'].logger_info(ob_class,target,comment)
+
+    @property
+    def state(self):
+        state = IWorkflowState(self).getState()
+        return state
+
+    @property
+    def history(self):
+        history = IObjectHistory(self)
+        return history
+
+# 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')
+
+class ApplicantTraverser(grok.Traverser):
+    """Get image of the context applicant.
+
+    Each applicant can provide a passport photograph which will be
+    returned by this traverser if:
+
+    - we request the exact filename of the picture or
+
+    - ask for a picture named 'passport.jpg'.
+
+    If no picture was stored yet, we get a placeholder image when
+    asking for `passport.jpg`.
+
+    If none of the above applies, we return ``None``, most probably
+    resulting a :exc:`NotFound` exception.
+
+    """
+    grok.context(IApplicant)
+    def traverse(self, name):
+        passport_filename = getattr(self.context.passport, 'filename', None)
+        if name == passport_filename:
+            return self.context.passport
+        if name == 'passport.jpg':
+            if self.context.passport is not None:
+                return self.context.passport
+        return
+
+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)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/authentication.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/authentication.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/authentication.py	(revision 6703)
@@ -0,0 +1,296 @@
+##
+## authentication.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Jul 27 14:26:35 2010 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2010 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public 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 authentication for applicants.
+
+   XXX: This is work in progress, experimental code! Don't do that at home!
+"""
+import grok
+from zope.event import notify
+from zope.pluggableauth.factories import Principal
+from zope.pluggableauth.interfaces import (
+    ICredentialsPlugin, IAuthenticatorPlugin,
+    IAuthenticatedPrincipalFactory, AuthenticatedPrincipalCreated)
+from zope.pluggableauth.plugins.session import SessionCredentialsPlugin
+from zope.publisher.interfaces import IRequest
+from zope.publisher.interfaces.http import IHTTPRequest
+from zope.session.interfaces import ISession
+from waeup.sirp.accesscodes import get_access_code
+from waeup.sirp.applicants.interfaces import (
+    IApplicantPrincipalInfo, IApplicantPrincipal, IApplicantSessionCredentials,
+    )
+from waeup.sirp.applicants import get_applicant_data
+from waeup.sirp.interfaces import IAuthPluginUtility
+
+
+class ApplicantPrincipalInfo(object):
+    """Infos about an applicant principal.
+    """
+    grok.implements(IApplicantPrincipalInfo)
+
+    def __init__(self, access_code):
+        self.id = principal_id(access_code)
+        self.title = u'Applicant'
+        self.description = u'An Applicant'
+        self.credentialsPlugin = None
+        self.authenticatorPlugin = None
+        self.access_code = access_code
+
+class ApplicantPrincipal(Principal):
+    """An applicant principal.
+
+    Applicant principals provide an extra `access_code` and `reg_no`
+    attribute extending ordinary principals.
+    """
+
+    grok.implements(IApplicantPrincipal)
+
+    def __init__(self, access_code, prefix=None):
+        self.id = principal_id(access_code)
+        if prefix is not None:
+            self.id = '%s.%s' % (prefix, self.id)
+        self.title = u'Applicant'
+        self.description = u'An applicant'
+        self.groups = []
+        self.access_code = access_code
+
+    def __repr__(self):
+        return 'ApplicantPrincipal(%r)' % self.id
+
+class AuthenticatedApplicantPrincipalFactory(grok.MultiAdapter):
+    """Creates 'authenticated' applicant principals.
+
+    Adapts (principal info, request) to an ApplicantPrincipal instance.
+
+    This adapter is used by the standard PAU to transform
+    PrincipalInfos into Principal instances.
+    """
+    grok.adapts(IApplicantPrincipalInfo, IRequest)
+    grok.implements(IAuthenticatedPrincipalFactory)
+
+    def __init__(self, info, request):
+        self.info = info
+        self.request = request
+
+    def __call__(self, authentication):
+        principal = ApplicantPrincipal(
+            self.info.access_code,
+            authentication.prefix,
+            )
+        notify(
+            AuthenticatedPrincipalCreated(
+                authentication, principal, self.info, self.request))
+        return principal
+
+
+#
+# Credentials plugins and related....
+#
+
+class ApplicantCredentials(object):
+    """Credentials class for ordinary applicants.
+    """
+    grok.implements(IApplicantSessionCredentials)
+
+    def __init__(self, access_code):
+        self.access_code = access_code
+
+    def getAccessCode(self):
+        """Get the access code.
+        """
+        return self.access_code
+
+    def getLogin(self):
+        """Stay compatible with non-applicant authenticators.
+        """
+        return None
+
+    def getPassword(self):
+        """Stay compatible with non-applicant authenticators.
+        """
+        return None
+
+class WAeUPApplicantCredentialsPlugin(grok.GlobalUtility,
+                                      SessionCredentialsPlugin):
+    """A credentials plugin that scans requests for applicant credentials.
+    """
+    grok.provides(ICredentialsPlugin)
+    grok.name('applicant_credentials')
+
+    loginpagename = 'login'
+    accesscode_prefix_field = 'form.ac_prefix'
+    accesscode_series_field = 'form.ac_series'
+    accesscode_number_field = 'form.ac_number'
+
+    def extractCredentials(self, request):
+        """Extracts credentials from a session if they exist.
+        """
+        if not IHTTPRequest.providedBy(request):
+            return None
+        session = ISession(request)
+        sessionData = session.get(
+            'zope.pluggableauth.browserplugins')
+        access_code_prefix = request.get(self.accesscode_prefix_field, None)
+        access_code_series = request.get(self.accesscode_series_field, None)
+        access_code_no = request.get(self.accesscode_number_field, None)
+        access_code = '%s-%s-%s' % (
+            access_code_prefix, access_code_series, access_code_no)
+        if None in [access_code_prefix, access_code_series, access_code_no]:
+            access_code = None
+        credentials = None
+
+        if access_code:
+            credentials = ApplicantCredentials(access_code)
+        elif not sessionData:
+            return None
+        sessionData = session[
+            'zope.pluggableauth.browserplugins']
+        if credentials:
+            sessionData['credentials'] = credentials
+        else:
+            credentials = sessionData.get('credentials', None)
+        if not credentials:
+            return None
+        if not IApplicantSessionCredentials.providedBy(credentials):
+            # If credentials were stored in session from another
+            # credentials plugin then we cannot make assumptions about
+            # its structure.
+            return None
+        return {'accesscode': credentials.getAccessCode()}
+
+
+
+class ApplicantsAuthenticatorPlugin(grok.GlobalUtility):
+    """Authenticate applicants.
+    """
+    grok.provides(IAuthenticatorPlugin)
+    grok.name('applicants')
+
+    def authenticateCredentials(self, credentials):
+        """Validate the given `credentials`
+
+        Credentials for applicants have to be passed as a regular
+        dictionary with a key ``accesscode``. This access code is the
+        password and username of an applicant.
+
+        Returns a :class:`ApplicantPrincipalInfo` in case of
+        successful validation, ``None`` else.
+
+        Credentials are not valid if:
+
+        - The passed accesscode does not exist (i.e. was not generated
+          by the :mod:`waeup.sirp.accesscode` module).
+
+        or
+
+        - the accesscode was disabled
+
+        or
+
+        - the accesscode was already used and a dataset for this
+          applicant was already generated with a different accesscode
+          (currently impossible, as applicant datasets are indexed by
+          accesscode)
+
+        or
+
+        - a dataset for the applicant already exists with an
+          accesscode set and this accesscode does not match the given
+          one.
+
+        """
+        if not isinstance(credentials, dict):
+            return None
+        accesscode = credentials.get('accesscode', None)
+        if accesscode is None:
+            return None
+        applicant_data = get_applicant_data(accesscode)
+        ac = get_access_code(accesscode) # Get the real access code object
+        appl_ac = getattr(applicant_data, 'access_code', None)
+        if ac is None:
+            return None
+        if ac.state == 'disabled':
+            return None
+        if ac.state == 'used' and appl_ac != ac.representation:
+            return None
+        if appl_ac is not None and appl_ac != ac.representation:
+            # This should never happen. It means a catalog error.
+            return None
+        return ApplicantPrincipalInfo(accesscode)
+
+    def principalInfo(self, id):
+        """Returns an IPrincipalInfo object for the specified principal id.
+
+        This method is used by the stadard PAU to lookup for instance
+        groups. If a principal belongs to a group, the group is looked
+        up by the id.  Currently we always return ``None``,
+        indicating, that the principal could not be found. This also
+        means, that is has no effect if applicant users belong to a
+        certain group. They can not gain extra-permissions this way.
+        """
+        return None
+
+class ApplicantsAuthUtility(grok.GlobalUtility):
+    """A global utility that sets up any PAU passed.
+
+    The methods of this utility are called during setup of a new site
+    (`University`) instance and after the regular authentication
+    systems (regular users, officers, etc.) were set up.
+    """
+    grok.provides(IAuthPluginUtility)
+    grok.name('applicants_auth_setup')
+
+    def register(self, pau):
+        """Register our local applicants specific PAU components.
+
+        Applicants provide their own authentication system resulting
+        in a specialized credentials plugin and a specialized
+        authenticator plugin.
+
+        Here we tell a given PAU that these plugins exist and should
+        be consulted when trying to authenticate a user.
+
+        We stack our local plugins at end of the plugin list, so that
+        other authentication mechanisms (the normal user
+        authentication for instance) have precedence and to avoid
+        "account-shadowing".
+        """
+        # The local credentials plugin is registered under the name
+        # 'applicant_credentials' (see above).
+        plugins = list(pau.credentialsPlugins) + ['applicant_credentials']
+        pau.credentialsPlugins = tuple(plugins)
+        # The local authenticator plugin is registered under the name
+        # 'applicants' (subject to change?)
+        plugins = list(pau.authenticatorPlugins) + ['applicants']
+        pau.authenticatorPlugins = tuple(plugins)
+        return pau
+
+    def unregister(self, pau):
+        return pau
+
+
+def principal_id(access_code):
+    """Get a principal ID for applicants.
+
+    We need unique principal ids for appliants. As access codes must
+    be unique we simply return them.
+    """
+    return access_code
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/batching.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/batching.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/batching.py	(revision 6703)
@@ -0,0 +1,69 @@
+##
+## batching.py
+## Login : <uli@pu.smp.net>
+## Started on  Fri Jul 23 15:42:46 2010 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2010 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public 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 grok
+from zope.interface import Interface
+from waeup.sirp.interfaces import IBatchProcessor
+from waeup.sirp.utils.batching import BatchProcessor
+from waeup.sirp.applicants.interfaces import IApplicantsContainer
+
+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
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser.py	(revision 6703)
@@ -0,0 +1,849 @@
+##
+## browser.py
+## Login : <uli@pu.smp.net>
+## Started on  Sun Jun 27 11:03:10 2010 Uli Fouquet & Henrik Bettermann
+## $Id$
+##
+## Copyright (C) 2010 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 sys
+import grok
+
+from datetime import datetime
+from zope.formlib.widget import CustomWidgetFactory
+from zope.formlib.form import setUpEditWidgets
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.traversing.browser import absoluteURL
+
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+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 waeup.sirp.accesscodes import invalidate_accesscode, get_access_code
+from waeup.sirp.accesscodes.workflow import USED
+from waeup.sirp.browser import (
+    WAeUPPage, WAeUPEditFormPage, WAeUPAddFormPage, WAeUPDisplayFormPage)
+from waeup.sirp.browser.breadcrumbs import Breadcrumb
+from waeup.sirp.browser.layout import NullValidator
+from waeup.sirp.browser.pages import add_local_role, del_local_roles
+from waeup.sirp.browser.resources import datepicker, tabs, datatable
+from waeup.sirp.browser.viewlets import (
+    ManageActionButton, PrimaryNavTab, LeftSidebarLink
+    )
+from waeup.sirp.image.browser.widget import (
+    ThumbnailWidget, EncodingImageFileWidget,
+    )
+from waeup.sirp.image.image import createWAeUPImageFile
+from waeup.sirp.interfaces import IWAeUPObject, ILocalRolesAssignable
+from waeup.sirp.permissions import get_users_with_local_roles
+from waeup.sirp.university.interfaces import ICertificate
+from waeup.sirp.widgets.datewidget import (
+    FriendlyDateWidget, FriendlyDateDisplayWidget)
+from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
+from waeup.sirp.widgets.objectwidget import (
+    WAeUPObjectWidget, WAeUPObjectDisplayWidget)
+from waeup.sirp.applicants import ResultEntry, Applicant, get_applicant_data
+from waeup.sirp.applicants.interfaces import (
+    IApplicant, IApplicantPrincipal,IApplicantEdit, IApplicantsRoot,
+    IApplicantsContainer, IApplicantsContainerAdd, application_types_vocab
+    )
+from waeup.sirp.applicants.workflow import INITIALIZED, STARTED
+
+results_widget = CustomWidgetFactory(
+    WAeUPObjectWidget, ResultEntry)
+
+results_display_widget = CustomWidgetFactory(
+    WAeUPObjectDisplayWidget, ResultEntry)
+
+class ApplicantsRootPage(WAeUPPage):
+    grok.context(IApplicantsRoot)
+    grok.name('index')
+    grok.require('waeup.Public')
+    title = 'Applicants'
+    label = 'Application Section'
+    pnav = 3
+
+    def update(self):
+        super(ApplicantsRootPage, self).update()
+        datatable.need()
+        return
+
+class ManageApplicantsRootActionButton(ManageActionButton):
+    grok.context(IApplicantsRoot)
+    grok.view(ApplicantsRootPage)
+    grok.require('waeup.manageApplications')
+    text = 'Manage application section'
+
+class ApplicantsRootManageFormPage(WAeUPEditFormPage):
+    grok.context(IApplicantsRoot)
+    grok.name('manage')
+    grok.template('applicantsrootmanagepage')
+    title = 'Applicants'
+    label = 'Manage application section'
+    pnav = 3
+    grok.require('waeup.manageApplications')
+    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()
+        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)
+
+    # ToDo: Show warning message before deletion
+    @grok.action('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: %s' % ', '.join(deleted))
+        self.redirect(self.url(self.context, '@@manage')+'#tab-1')
+        return
+
+    @grok.action('Add applicants container', validator=NullValidator)
+    def addApplicantsContainer(self, **data):
+        self.redirect(self.url(self.context, '@@add'))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @grok.action('Add local role', validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self,2, **data)
+
+    @grok.action('Remove selected local roles')
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,2,**data)
+
+class ApplicantsContainerAddFormPage(WAeUPAddFormPage):
+    grok.context(IApplicantsRoot)
+    grok.require('waeup.manageApplications')
+    grok.name('add')
+    grok.template('applicantscontaineraddpage')
+    title = 'Applicants'
+    label = 'Add applicants container'
+    pnav = 3
+
+    form_fields = grok.AutoFields(
+        IApplicantsContainerAdd).omit('code').omit('title')
+    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()
+
+    @grok.action('Add applicants container')
+    def addApplicantsContainer(self, **data):
+        year = data['year']
+        code = u'%s%s' % (data['prefix'], year)
+        prefix = application_types_vocab.getTerm(data['prefix'])
+        title = u'%s %s/%s' % (prefix.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')+'#tab-1')
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context, '@@manage') + '#tab-1')
+
+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.access_code
+
+class ApplicantsTab(PrimaryNavTab):
+    """Applicants tab in primary navigation.
+    """
+
+    grok.context(IWAeUPObject)
+    grok.order(3)
+    grok.require('waeup.Public')
+    grok.template('primarynavtab')
+
+    pnav = 3
+    tab_title = u'Applicants'
+
+    @property
+    def link_target(self):
+        return self.view.application_url('applicants')
+
+class ApplicantsContainerPage(WAeUPDisplayFormPage):
+    """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')
+    form_fields['startdate'].custom_widget = FriendlyDateDisplayWidget('le')
+    form_fields['enddate'].custom_widget = FriendlyDateDisplayWidget('le')
+    form_fields['description'].custom_widget = ReSTDisplayWidget
+
+    @property
+    def title(self):
+        return "Applicants Container: %s" % self.context.title
+
+    @property
+    def label(self):
+        return self.context.title
+
+class ApplicantsContainerManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsContainerPage)
+    grok.require('waeup.manageApplications')
+    text = 'Manage applicants container'
+
+class LoginApplicantActionButton(ManageActionButton):
+    grok.order(2)
+    grok.context(IApplicantsContainer)
+    grok.view(ApplicantsContainerPage)
+    grok.require('waeup.Anonymous')
+    icon = 'login.png'
+    text = 'Login for applicants'
+    target = 'login'
+
+class ApplicantsContainerManageFormPage(WAeUPEditFormPage):
+    grok.context(IApplicantsContainer)
+    grok.name('manage')
+    grok.template('applicantscontainermanagepage')
+    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
+    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.manageApplications')
+
+    @property
+    def title(self):
+        return "Applicants Container: %s" % self.context.title
+
+    @property
+    def label(self):
+        return 'Manage applicants container'
+
+    pnav = 3
+
+    def update(self):
+        datepicker.need() # Enable jQuery datepicker in date fields.
+        tabs.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)
+
+    @grok.action('Save')
+    def apply(self, **data):
+        self.applyData(self.context, **data)
+        self.flash('Data saved.')
+        return
+
+    # ToDo: Show warning message before deletion
+    @grok.action('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')+'#tab-2')
+            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: %s' % ', '.join(deleted))
+        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
+        return
+
+    @grok.action('Add applicant', validator=NullValidator)
+    def addApplicant(self, **data):
+        self.redirect(self.url(self.context, 'addapplicant'))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @grok.action('Add local role', validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self,3, **data)
+
+    @grok.action('Remove selected local roles')
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,3,**data)
+
+class LoginApplicant(WAeUPPage):
+    grok.context(IApplicantsContainer)
+    grok.name('login')
+    grok.require('waeup.Public')
+
+    @property
+    def title(self):
+        return u"Applicant Login: %s" % self.context.title
+
+    @property
+    def label(self):
+        return u'Login for applicants only'
+
+    pnav = 3
+
+    @property
+    def ac_prefix(self):
+        return self.context.ac_prefix
+
+    def update(self, SUBMIT=None):
+        self.ac_series = self.request.form.get('form.ac_series', None)
+        self.ac_number = self.request.form.get('form.ac_number', None)
+        if SUBMIT is None:
+            return
+        if self.request.principal.id == 'zope.anybody':
+            self.flash('Entered credentials are invalid.')
+            return
+        if not IApplicantPrincipal.providedBy(self.request.principal):
+            # Don't care if user is already authenticated as non-applicant
+            return
+
+        # From here we handle an applicant (not an officer browsing)
+        pin = self.request.principal.access_code
+
+        # Mark pin as used (this also fires a pin related transition)
+        if get_access_code(pin).state == USED:
+            pass
+        else:
+            comment = u"AC invalidated"
+            # Here we know that the ac is in state initialized so we do not
+            # expect an exception
+            invalidate_accesscode(pin,comment)
+
+        if not pin in self.context.keys():
+            # Create applicant record
+            applicant = Applicant()
+            applicant.access_code = pin
+            self.context[pin] = applicant
+
+        role_manager = IPrincipalRoleManager(self.context[pin])
+        role_manager.assignRoleToPrincipal(
+            'waeup.local.ApplicationOwner', self.request.principal.id)
+
+        # Assign current principal the PortalUser role
+        role_manager = IPrincipalRoleManager(grok.getSite())
+        role_manager.assignRoleToPrincipal(
+            'waeup.PortalUser', self.request.principal.id)
+
+        # Mark application as started
+        if IWorkflowState(self.context[pin]).getState() is INITIALIZED:
+            IWorkflowInfo(self.context[pin]).fireTransition('start')
+
+        self.redirect(self.url(self.context[pin], 'edit'))
+        return
+
+class ApplicantAddFormPage(WAeUPAddFormPage):
+    """Add-form to add an applicant.
+    """
+    grok.context(IApplicantsContainer)
+    grok.require('waeup.manageApplications')
+    grok.name('addapplicant')
+    grok.template('applicantaddpage')
+    title = 'Applicants'
+    label = 'Add applicant'
+    pnav = 3
+
+    @property
+    def title(self):
+        return "Applicants Container: %s" % self.context.title
+
+    @property
+    def ac_prefix(self):
+        return self.context.ac_prefix
+
+    @grok.action('Create application record')
+    def addApplicant(self, **data):
+        ac_series = self.request.form.get('form.ac_series', None)
+        ac_number = self.request.form.get('form.ac_number', None)
+        pin = '%s-%s-%s' % (self.ac_prefix,ac_series,ac_number)
+        if not invalidate_accesscode(pin, comment=u"Invalidated by system"):
+            self.flash('%s is not a valid access code.' % pin)
+            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
+            return
+        else:
+            # Create applicant record
+            applicant = Applicant()
+            applicant.access_code = pin
+            self.context[pin] = applicant
+        self.redirect(self.url(self.context[pin], 'edit'))
+        return
+
+class AccessCodeViewLink(LeftSidebarLink):
+    grok.order(1)
+    grok.require('waeup.Public')
+    icon = 'actionicon_view.png'
+    title = 'View Record'
+    target = '/@@index'
+
+    @property
+    def url(self):
+        if not IApplicantPrincipal.providedBy(self.request.principal):
+            return ''
+        access_code = getattr(self.request.principal,'access_code',None)
+        if access_code:
+            applicant_object = get_applicant_data(access_code)
+            return absoluteURL(applicant_object, self.request) + self.target
+        #else:
+        #    return ''
+
+class AccessCodeEditLink(AccessCodeViewLink):
+    grok.order(2)
+    grok.require('waeup.Public')
+    icon = 'actionicon_modify.png'
+    title = 'Edit Record'
+    target = '/@@edit'
+
+    @property
+    def url(self):
+        if not IApplicantPrincipal.providedBy(self.request.principal):
+            return ''
+        access_code = getattr(self.request.principal,'access_code',None)
+        if access_code:
+            applicant_object = get_applicant_data(access_code)
+            if applicant_object.locked:
+                return ''
+            return absoluteURL(applicant_object, self.request) + self.target
+        #else:
+        #    return ''
+
+class AccessCodeSlipLink(AccessCodeViewLink):
+    grok.order(3)
+    grok.require('waeup.Public')
+    icon = 'actionicon_pdf.png'
+    title = 'Download Slip'
+    target = '/application_slip.pdf'
+
+class DisplayApplicant(WAeUPDisplayFormPage):
+    grok.context(IApplicant)
+    grok.name('index')
+    grok.require('waeup.handleApplication')
+    form_fields = grok.AutoFields(IApplicant).omit(
+        'locked').omit('course_admitted')
+    #form_fields['fst_sit_results'].custom_widget = list_results_display_widget
+    form_fields['passport'].custom_widget = ThumbnailWidget
+    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
+    label = 'Applicant'
+    grok.template('form_display')
+    pnav = 3
+
+    @property
+    def title(self):
+        if self.request.principal.title == 'Applicant':
+            return u'Your Application Record'
+        return '%s' % self.context.access_code
+
+    @property
+    def label(self):
+        container_title = self.context.__parent__.title
+        return '%s Application Record' % container_title
+
+    def getCourseAdmitted(self):
+        """Return link, title and code in html format to the certificate
+           admitted.
+        """
+        course_admitted = self.context.course_admitted
+        if ICertificate.providedBy(course_admitted):
+            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 PDFActionButton(ManageActionButton):
+    grok.context(IApplicant)
+    #grok.view(DisplayApplicant)
+    grok.require('waeup.manageApplications')
+    icon = 'actionicon_pdf.png'
+    text = 'Download application slip'
+    target = 'application_slip.pdf'
+
+class ExportPDFPage(grok.View):
+    """Deliver a PDF slip of the context.
+    """
+    grok.context(IApplicant)
+    grok.name('application_slip.pdf')
+    grok.require('waeup.handleApplication')
+    form_fields = grok.AutoFields(IApplicant).omit(
+        'locked').omit('course_admitted')
+    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
+    prefix = 'form'
+
+    @property
+    def label(self):
+        container_title = self.context.__parent__.title
+        return '%s Application Record' % container_title
+
+    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):
+            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):
+        # (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
+        SLIP_STYLE = TableStyle(
+            [('VALIGN',(0,0),(-1,-1),'TOP')]
+            )
+
+        pdf = canvas.Canvas('application_slip.pdf',pagesize=A4)
+        pdf.setTitle(self.label)
+        pdf.setSubject('Application')
+        pdf.setAuthor('%s (%s)' % (self.request.principal.title,
+            self.request.principal.id))
+        pdf.setCreator('WAeUP 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.label, style["Heading2"]))
+        #story.append(HRFlowable())
+        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))
+        self.setUpWidgets()
+        data = []
+        for widget in self.widgets:
+            f_label = '<font size=12>%s</font>:' % widget.label.strip()
+            f_label = Paragraph(f_label, style["Normal"])
+            if widget.name != 'form.passport':
+                f_text = '<font size=12>%s</font>' % widget()
+                f_text = Paragraph(f_text, style["Normal"])
+                data.append([f_label,f_text])
+            else:
+                filename = widget._data.file.name
+                im = Image(filename,width=4*cm, height=3*cm,kind='bound')
+                data.append([f_label,im])
+        f_label = '<font size=12>Admitted Course of Study:</font>'
+        f_text = '<font size=12>%s</font>' % self.getCourseAdmitted()
+        f_label = Paragraph(f_label, style["Normal"])
+        f_text = Paragraph(f_text, style["Normal"])
+        data.append([f_label,f_text])
+        table = Table(data,style=SLIP_STYLE)
+        story.append(table)
+        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)
+
+        self.response.setHeader(
+            'Content-Type', 'application/pdf')
+        return pdf.getpdfdata()
+
+class ApplicantManageActionButton(ManageActionButton):
+    grok.context(IApplicant)
+    grok.view(DisplayApplicant)
+    grok.require('waeup.manageApplications')
+    text = 'Manage application record'
+    target = 'edit_full'
+
+class EditApplicantFull(WAeUPEditFormPage):
+    """A full edit view for applicant data.
+    """
+    grok.context(IApplicant)
+    grok.name('edit_full')
+    grok.require('waeup.manageApplications')
+    form_fields = grok.AutoFields(IApplicant)
+    form_fields['passport'].custom_widget = EncodingImageFileWidget
+    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
+    grok.template('form_edit')
+    manage_applications = True
+    pnav = 3
+
+    def update(self):
+        datepicker.need() # Enable jQuery datepicker in date fields.
+        super(EditApplicantFull, self).update()
+        self.wf_info = IWorkflowInfo(self.context)
+        upload = self.request.form.get('form.passport', None)
+        if upload:
+            # We got a fresh upload
+            image = createWAeUPImageFile(upload.filename, upload)
+            # This would normally be totally illegal. We set context
+            # data without the complete form data being
+            # verified. Normally data is set in methods like `save`
+            # only, which is only called when all form data is correct
+            # (and if not, the context is not touched). With images
+            # and their uploads the problem then is, that the uploaded
+            # image won't be shown in a partially erranous form
+            # because the new uploaded image is still not part of the
+            # context obeject and the image widget cannot work around
+            # that (it has no access to the new data). For this reason
+            # we set the image here and not in 'save()' or other
+            # methods.
+            self.context.passport = image
+            upload.seek(0)
+            self.passport_changed = True
+        return
+
+    @property
+    def title(self):
+        return self.context.access_code
+
+    @property
+    def label(self):
+        container_title = self.context.__parent__.title
+        return '%s Application Form' % container_title
+
+    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]
+
+    @grok.action('Save')
+    def save(self, **data):
+        changed_fields = self.applyData(self.context, **data)
+        changed_fields = changed_fields.values()
+        fields_string = '+'.join(' + '.join(str(i) for i in b) for b in changed_fields)
+        self.context._p_changed = True
+        form = self.request.form
+        if form.has_key('transition') and form['transition']:
+            transition_id = form['transition']
+            self.wf_info.fireTransition(transition_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
+
+class EditApplicantStudent(EditApplicantFull):
+    """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',
+        )
+    form_fields['passport'].custom_widget = EncodingImageFileWidget
+    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
+    grok.template('form_edit')
+    manage_applications = False
+    title = u'Your Application Form'
+
+
+    def emitLockMessage(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.emitLockMessage()
+            return
+        datepicker.need() # Enable jQuery datepicker in date fields.
+        upload = self.request.form.get('form.passport', None)
+        if upload:
+            # We got a fresh upload
+            image = createWAeUPImageFile(upload.filename, upload)
+            # This would normally be totally illegal. We set context
+            # data without the complete form data being
+            # verified. Normally data is set in methods like `save`
+            # only, which is only called when all form data is correct
+            # (and if not, the context is not touched). With images
+            # and their uploads the problem then is, that the uploaded
+            # image won't be shown in a partially erranous form
+            # because the new uploaded image is still not part of the
+            # context obeject and the image widget cannot work around
+            # that (it has no access to the new data). For this reason
+            # we set the image here and not in 'save()' or other
+            # methods.
+            self.context.passport = image
+            upload.seek(0)
+            self.passport_changed = True
+        super(EditApplicantStudent, self).update()
+        return
+
+    def dataNotComplete(self):
+        if not self.request.form.get('confirm_passport', False):
+            return 'Passport confirmation box not ticked.'
+        #if len(self.errors) > 0:
+        #    return 'Form has errors.'
+        return False
+
+    @grok.action('Save')
+    def save(self, **data):
+        #if self.context.locked:
+        #    self.emitLockMessage()
+        #    return
+        self.applyData(self.context, **data)
+        self.context._p_changed = True
+        self.flash('Form has been saved.')
+        return
+
+    @grok.action('Final Submit')
+    def finalsubmit(self, **data):
+        #if self.context.locked:
+        #    self.emitLockMessage()
+        #    return
+        self.applyData(self.context, **data)
+        self.context._p_changed = True
+        if self.dataNotComplete():
+            self.flash(self.dataNotComplete())
+            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 != STARTED:
+            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 ApplicantViewActionButton(ManageActionButton):
+    grok.context(IApplicant)
+    grok.view(EditApplicantFull)
+    grok.require('waeup.manageApplications')
+    icon = 'actionicon_view.png'
+    text = 'View application record'
+    target = 'index'
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantaddpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantaddpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantaddpage.pt	(revision 6703)
@@ -0,0 +1,29 @@
+<h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+<form method="POST">
+  <table>
+    <tbody>
+      <tr>
+    	<td><label for="form.ac_series">Access Code:</label></td>
+    	<td>
+          <span tal:replace="view/ac_prefix">PIN Prefix</span>
+         -<input name="form.ac_series" type="text" size="3" maxlength="3"
+                  value="" />
+         -<input name="form.ac_number" type="text" size="10" maxlength="10"
+                value="" />
+    	</td>
+      </tr>
+      <tr>
+        <td>
+            <div class="actionButtons" tal:condition="view/availableActions">
+                <span tal:repeat="action view/actions" tal:omit-tag="">
+                    <input tal:replace="structure action/render"/>
+                </span>
+            </div>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantscontaineraddpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantscontaineraddpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantscontaineraddpage.pt	(revision 6703)
@@ -0,0 +1,48 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      class="edit-form" enctype="multipart/form-data">
+
+  <h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+  <div class="form-status"
+    tal:define="status view/status"
+    tal:condition="status">
+    Form Status:
+    <span i18n:translate="" tal:content="view/status">
+      Form status summary
+    </span>
+  </div>
+
+  <table class="zebra">
+    <tbody>
+      <tal:block repeat="widget view/widgets">
+        <tr>
+          <td class="label">
+            <label tal:attributes="for widget/name">
+              <span class="required" tal:condition="widget/required">*</span>
+              <span i18n:translate="" tal:content="widget/label">label</span>:
+            </label>
+          </td>
+          <td class="field">
+            <span class="widget" 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">
+              <span class="hint" tal:content="structure widget/hint">hint</span>
+            </tal:hint>
+          </td>
+        </tr>
+      </tal:block>
+    </tbody>
+  </table>
+
+  <div class="actionButtons" tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+             />
+  </div>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantscontainermanagepage.pt	(revision 6703)
@@ -0,0 +1,137 @@
+<form action="." tal:attributes="action request/URL" method="POST"  class="edit-form" enctype="multipart/form-data">
+  <h2 i18n:translate="" tal:condition="view/label" tal:content="view/label">Label</h2>
+  <div class="form-status"  tal:define="status view/status"  tal:condition="status"> Form Status:
+    <span i18n:translate="" tal:content="view/status"> Form status summary
+    </span>
+  </div>
+  <div id="tabs">
+    <ul>
+      <li>
+      <a href="#tab-1">
+        <span>Settings
+        </span></a>
+      </li>
+      <li>
+      <a href="#tab-2">
+        <span>Applicants
+        </span></a>
+      </li>
+      <li>
+      <a href="#tab-3">
+        <span>Local Roles
+        </span></a>
+      </li>
+    </ul>
+    <div id="tab-1">
+      <table class="zebra">
+        <tbody>
+          <tal:block repeat="widget view/widgets">
+            <tr>
+              <td class="label">
+                <label tal:attributes="for widget/name">
+                  <span class="required" tal:condition="widget/required">*
+                  </span>
+                  <span i18n:translate="" tal:content="widget/label">label
+                  </span>:
+                </label> </td>
+              <td class="field">
+                <span class="widget" 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">
+                  <span class="hint" tal:content="structure widget/hint">hint
+                  </span>
+                </tal:hint> </td>
+            </tr>
+          </tal:block>
+        </tbody>
+      </table>
+      <div class="actionButtons" 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"> <h3>Applicants</h3>
+      <table class="display dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th>ID
+            </th>
+            <th>state
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr 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__"> ID </a> </td> <td>
+              <span tal:replace="appl/state">state
+              </span> </td>
+          </tr>
+        </tbody>
+      </table>
+      <div class="actionButtons" 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"> <br />
+      <table class="display dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th>User Id
+            </th>
+            <th>Name
+            </th>
+            <th>Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles" class="gradeB"> <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 class="actionButtons" 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="zebra">
+        <tr> <td>
+            <select id="user" name="user">
+              <option tal:repeat="user view/getUsers" tal:attributes="value user/name">
+              <span tal:replace="user/val/description">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/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantscontainerpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantscontainerpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantscontainerpage.pt	(revision 6703)
@@ -0,0 +1,22 @@
+<h2 tal:content="view/label">Title</h2>
+
+<span tal:replace="structure view/widgets/description">Description</span>
+
+<br />
+
+<table class="zebra" 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 class="field">
+          <tal:block tal:replace="structure widget" />
+        </td>
+      </tr>
+      </tal:condition>
+    </tal:block>
+  </tbody>
+</table>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantsrootmanagepage.pt	(revision 6703)
@@ -0,0 +1,107 @@
+<h2 i18n:translate="" tal:condition="view/label" tal:content="view/label">Label</h2>
+<div class="form-status" tal:define="status view/status" tal:condition="status">
+  <div i18n:translate="" tal:content="view/status"> Form status summary
+  </div>
+  <ul class="errors" tal:condition="view/errors">
+    <li tal:repeat="error view/error_views">
+      <span tal:replace="structure error">Error Type
+      </span>
+    </li>
+  </ul>
+</div>
+<form action="." tal:attributes="action request/URL" method="POST"  class="edit-form" enctype="multipart/form-data">
+  <div id="tabs">
+    <ul>
+      <li>
+      <a href="#tab-1">
+        <span tal:content="view/subunits">Contents
+        </span></a>
+      </li>
+      <li>
+      <a href="#tab-2">
+        <span>Local Roles
+        </span></a>
+      </li>
+    </ul>
+    <div id="tab-1">
+      <h3 tal:content="view/subunits">Applicants Containers</h3>
+      <table class="display dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th>Year
+            </th>
+            <th>Code
+            </th>
+            <th>Title
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry context/values" 	class="gradeB"> <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 class="actionButtons" 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>User Id
+            </th>
+            <th>Name
+            </th>
+            <th>Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles" class="gradeB">  <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 class="actionButtons" 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="zebra">
+        <tr> <td>
+            <select id="user" name="user">
+              <option tal:repeat="user view/getUsers" tal:attributes="value user/name">
+              <span tal:replace="user/val/description">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/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantsrootpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantsrootpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/applicantsrootpage.pt	(revision 6703)
@@ -0,0 +1,25 @@
+<h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+<table class="display dataTable">
+  <thead>
+    <tr>
+      <th>Year</th><th>Code</th><th>Title</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="entry context/values"
+	class="gradeB">
+      <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>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/form_display.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/form_display.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/form_display.pt	(revision 6703)
@@ -0,0 +1,34 @@
+<h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+<h2> ... <span tal:replace="context/state">Application State</span> ... </h2>
+
+<div class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">message</span><br />
+  </div>
+</div>
+
+<table class="zebra">
+  <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">
+          Admitted Course of Study:
+      </td>
+      <td>
+        <span tal:replace="structure view/getCourseAdmitted" />
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/form_edit.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/form_edit.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/form_edit.pt	(revision 6703)
@@ -0,0 +1,74 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      class="edit-form" enctype="multipart/form-data">
+
+  <h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+  <h2> ... <span tal:replace="context/state">Application State</span> ... </h2>
+
+  <div class="workflow">
+    <div tal:repeat="msg context/history/messages">
+      <span tal:replace="msg">message</span><br />
+    </div>
+  </div>
+
+  <div class="form-status"
+    tal:define="status view/status"
+    tal:condition="status">
+    Form Status:
+    <span i18n:translate="" tal:content="view/status">
+      Form status summary
+    </span>
+  </div>
+
+  <table class="form-fields zebra">
+    <tbody>
+      <tal:block repeat="widget view/widgets">
+        <tr>
+          <td class="label">
+            <label tal:attributes="for widget/name">
+              <span class="required" tal:condition="widget/required">*</span>
+              <span i18n:translate="" tal:content="widget/label">label</span>:
+            </label>
+          </td>
+          <td class="field">
+            <span class="widget" 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">
+              <span class="hint" tal:content="structure widget/hint">hint</span>
+            </tal:hint>
+          </td>
+        </tr>
+      </tal:block>
+      <tr tal:condition="view/manage_applications">
+        <td class="label"><label>Application Transition:</label></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 class="checkboxType" id="confirm_passport" name="confirm_passport"
+           type="checkbox" value="True"/>
+    I confirm that the Passport Photograph uploaded on this form is a
+    true picture of me.
+  </div>
+
+  <div class="actionButtons" tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/loginapplicant.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/loginapplicant.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/loginapplicant.pt	(revision 6703)
@@ -0,0 +1,56 @@
+<h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+
+<form method="POST">
+  <input type="hidden" name="form.ac_prefix"
+         value="" tal:attributes="value view/ac_prefix" />
+  <table>
+    <tbody>
+      <tr>
+    	<td><label for="form.ac_series">Access Code:</label></td>
+    	<td>
+          <span tal:replace="view/ac_prefix">PIN Prefix</span>
+         -<input name="form.ac_series" type="text" size="3" maxlength="3"
+                  value="" tal:attributes="value view/ac_series"/>
+         -<input name="form.ac_number" type="text" size="10" maxlength="10"
+                value="" tal:attributes="value view/ac_number" />
+    	</td>
+      </tr>
+      <tr>
+	<td colspan="2">
+	  <input type="submit" name="SUBMIT" value="Show Application Record" />
+	</td>
+      </tr>
+    </tbody>
+  </table>
+</form>
+
+<div>
+  <h3>Instructions:</h3>
+  <ul class="list">
+    <li>
+      Enter your Application PIN.
+    </li>
+    <li>
+      Fill the form and upload your Passport Photograph with red
+      background in jpg format.
+    </li>
+    <li>
+      Save and preview picture.
+    </li>
+    <li>
+      Press the 'Final Submit' button.
+    </li>
+    <li>
+      After submission you won't be able to edit the data
+      but you can access and print this form again.
+    </li>
+  </ul>
+</div>
+<div tal:on-error="nothing">
+  <div class="error" tal:content="view/getDeadline">
+    The deadline is NOW
+  </div>
+</div>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/primarynavtab.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/primarynavtab.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/browser_templates/primarynavtab.pt	(revision 6703)
@@ -0,0 +1,7 @@
+<li tal:attributes="class viewlet/active">
+  <a href="#"
+     tal:attributes="href viewlet/link_target"
+     tal:content="viewlet/tab_title">
+    Primary Tab Title
+  </a>
+</li>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/container.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/container.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/container.py	(revision 6703)
@@ -0,0 +1,102 @@
+##
+## container.py
+## Login : <uli@pu.smp.net>
+## Started on  Thu Jan 20 04:33:18 2011 Uli Fouquet
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.
+"""
+import grok
+from zope.component.factory import Factory
+from zope.component.interfaces import IFactory
+from waeup.sirp.applicants.interfaces import (
+    IApplicantsContainer, IApplicantsContainerAdd,
+    IApplicantsContainerProvider,
+    )
+from waeup.sirp.utils.helpers import attrs_to_fields
+
+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 ['waeup.ApplicationsOfficer']
+
+    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()
+
+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/uli-studentpw/src/waeup/sirp/applicants/interfaces.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/interfaces.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/interfaces.py	(revision 6703)
@@ -0,0 +1,561 @@
+##
+## interfaces.py
+## Login : <uli@pu.smp.net>
+## Started on  Sun Jan 16 15:30:01 2011 Uli Fouquet
+## $Id$
+##
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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.
+"""
+import os
+import re
+import waeup.sirp.browser
+
+from grokcore.content.interfaces import IContainer
+
+from zope import schema
+from zope.interface import Interface, Attribute, provider
+from zope.component import getUtilitiesFor
+from zope.pluggableauth.interfaces import IPrincipalInfo
+from zope.security.interfaces import IGroupClosureAwarePrincipal as IPrincipal
+from zc.sourcefactory.basic import BasicSourceFactory
+from waeup.sirp.image import createWAeUPImageFile
+from waeup.sirp.image.schema import ImageFile
+from waeup.sirp.interfaces import IWAeUPObject
+from waeup.sirp.university.vocabularies import application_categories
+from waeup.sirp.students.vocabularies import (
+  year_range, lgas_vocab, CertificateSource, GenderSource,
+  )
+from waeup.sirp.applicants.vocabularies import (
+  application_types_vocab, application_pins_vocab,
+  AppCatCertificateSource,
+  )
+
+IMAGE_PATH = os.path.join(
+    os.path.dirname(waeup.sirp.browser.__file__),
+    'static'
+    )
+DEFAULT_PASSPORT_IMAGE_MALE = open(
+    os.path.join(IMAGE_PATH, 'placeholder_m.jpg')).read()
+DEFAULT_PASSPORT_IMAGE_FEMALE = open(
+    os.path.join(IMAGE_PATH, 'placeholder_f.jpg')).read()
+
+# 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
+
+@provider(schema.interfaces.IContextAwareDefaultFactory)
+def default_passport_image(context):
+    """A default value factory for ImageFile fields.
+
+    Returns some default image as WAeUPImageFile. We cannot set the
+    default directly in ImageFile fields, as, if we want to set
+    max_size or min_size as well, some utility lookups are needed
+    which are not possible during startup.
+
+    Developers which use IContextAwareDefaultFactories like this one
+    always should make sure that the delivered default meets all
+    constraints of the field that makes use of this default value
+    provider.
+    """
+    imagefile = createWAeUPImageFile(
+        'placeholder_m.jpg',
+        open(os.path.join(IMAGE_PATH, 'placeholder_m.jpg'), 'r')
+        )
+    return imagefile
+
+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 IResultEntry(IWAeUPObject):
+    subject = schema.TextLine(
+        title = u'Subject',
+        description = u'The subject',
+        required=False,
+        )
+    score = schema.TextLine(
+        title = u'Score',
+        description = u'The score',
+        required=False,
+        )
+
+class IApplicantsRoot(IWAeUPObject, IContainer):
+    """A container for university applicants containers.
+    """
+    pass
+
+class IApplicantsContainer(IWAeUPObject):
+    """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 = application_types_vocab,
+        readonly = 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,
+        )
+
+    ac_prefix = schema.Choice(
+        title = u'Access code prefix',
+        required = True,
+        default = None,
+        source = application_pins_vocab,
+        )
+
+    application_category = schema.Choice(
+        title = u'Category for the grouping of certificates',
+        required = True,
+        default = None,
+        source = application_categories,
+        )
+
+    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 information about the study courses provided, the application procedure and deadlines.'''
+        )
+
+    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 = application_types_vocab,
+        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(IWAeUPObject):
+    """The data for an applicant.
+
+    This is a base interface with no field (except ``reg_no``)
+    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.
+    """
+    history = Attribute('Object history, a list of messages.')
+    state = Attribute('Returns the application state of an applicant')
+    application_date = Attribute('Date of submission, used for export only')
+
+    #def getApplicantsRootLogger():
+    #    """Returns the logger from the applicants root object
+    #    """
+
+    def loggerInfo(ob_class, comment):
+        """Adds an INFO message to the log file
+        """
+
+    reg_no = schema.TextLine(
+        title = u'JAMB Registration Number',
+        readonly = True,
+        )
+    access_code = schema.TextLine(
+        title = u'Access Code',
+        required = False,
+        readonly = True,
+        )
+    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,
+        )
+    firstname = schema.TextLine(
+        title = u'First Name',
+        required = True,
+        )
+    middlenames = schema.TextLine(
+        title = u'Middle Names',
+        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',
+        required = False,
+        constraint=validate_email,
+        )
+    phone = schema.Int(
+        title = u'Phone',
+        description = u'Enter phone number with country code and without spaces.',
+        required = False,
+        )
+    passport = ImageFile(
+        title = u'Passport Photograph',
+        #default = DEFAULT_PASSPORT_IMAGE_MALE,
+        defaultFactory = default_passport_image,
+        description = u'Maximun file size is 20 kB.',
+        required = True,
+        max_size = 20480,
+        )
+
+    #
+    # Process Data
+    #
+    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,
+        )
+    student_id = schema.TextLine(
+        title = u'Student ID',
+        required = False,
+        readonly = True,
+        )
+    locked = schema.Bool(
+        title = u'Form locked',
+        default = False,
+        )
+
+class IApplicant(IApplicantBaseData):
+    """An applicant.
+
+    This is basically the applicant base data. Here we repeat the
+    fields from base data if we have to set the `required` attribute
+    to True (which is the default).
+    """
+
+class IApplicantEdit(IApplicantBaseData):
+    """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. We cannot omit fields. This has to be done in the
+    respective form page.
+    """
+    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,
+        )
+    # entry_session is inherited from the container
+    #entry_session = schema.Choice(
+    #    source = entry_session_vocab,
+    #    title = u'Entry Session',
+    #    required = False,
+    #    readonly = True
+    #    )
+    notice = schema.Text(
+        title = u'Notice',
+        required = False,
+        readonly = True,
+        )
+
+class IApplicantPrincipalInfo(IPrincipalInfo):
+    """Infos about principals that are applicants.
+    """
+    access_code = Attribute("The Access Code the user purchased")
+
+class IApplicantPrincipal(IPrincipal):
+    """A principal that is an applicant.
+
+    This interface extends zope.security.interfaces.IPrincipal and
+    requires also an `id` and other attributes defined there.
+    """
+    access_code = schema.TextLine(
+        title = u'Access Code',
+        description = u'The access code purchased by the user.',
+        required = True,
+        readonly = True)
+
+class IApplicantsFormChallenger(Interface):
+    """A challenger that uses a browser form to collect applicant
+       credentials.
+    """
+    loginpagename = schema.TextLine(
+        title = u'Loginpagename',
+        description = u"""Name of the login form used by challenger.
+
+        The form must provide an ``access_code`` input field.
+        """)
+
+    accesscode_field = schema.TextLine(
+        title = u'Access code field',
+        description = u'''Field of the login page which is looked up for
+                          access_code''',
+        default = u'access_code',
+        )
+
+
+class IApplicantSessionCredentials(Interface):
+    """Interface for storing and accessing applicant credentials in a
+       session.
+    """
+
+    def __init__(access_code):
+        """Create applicant session credentials."""
+
+    def getAccessCode():
+        """Return the access code."""
+
+
+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/uli-studentpw/src/waeup/sirp/applicants/permissions.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/permissions.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/permissions.py	(revision 6703)
@@ -0,0 +1,48 @@
+##
+## permissions.py
+## Login : <uli@pu.smp.net>
+## Started on  Wed May 11 13:05:37 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public 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 ManageApplications(grok.Permission):
+    grok.name('waeup.manageApplications')
+
+# Local role
+class ApplicationOwner(grok.Role):
+    grok.name('waeup.local.ApplicationOwner')
+    grok.title(u'Application Owner')
+    grok.permissions('waeup.handleApplication', 'waeup.viewApplication')
+
+# Global role
+class ApplicationsOfficer(grok.Role):
+    grok.name('waeup.ApplicationsOfficer')
+    grok.title(u'Applications Officer')
+    grok.permissions('waeup.manageApplications', 'waeup.viewApplication')
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/root.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/root.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/root.py	(revision 6703)
@@ -0,0 +1,143 @@
+##
+## root.py
+## Login : <uli@pu.smp.net>
+## Started on  Thu Jan 20 04:17:59 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public 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 IWAeUPSIRPPluggable
+from waeup.sirp.applicants.interfaces import IApplicantsRoot
+from waeup.sirp.utils.helpers import get_current_principal
+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 = ['waeup.ApplicationsOfficer']
+
+    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.
+        """
+        user = get_current_principal()
+        if user is None:
+            user = 'system'
+        elif user.title == 'Applicant':
+            user = 'applicant'
+        else:
+            user = user.id
+        self.logger.info('%s - %s - %s - %s' % (
+                user, target, ob_class, comment))
+        return
+
+class ApplicantsPlugin(grok.GlobalUtility):
+    """A WAeUPSIRPPlugin 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.IWAeUPSIRPPluggable`
+    """
+    grok.name('applicants')
+    grok.implements(IWAeUPSIRPPluggable)
+    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
+
+def get_applicant_data(identifier):
+    """Get applicant data associated with `identifier`.
+
+    Returns the applicant object if successful and ``None`` else.
+
+    As `identifier` we expect an access code in format
+    like ``PREFIX-XXX-YYYYYYYY`` where ``PREFIX`` is something like
+    ``APP`` or ``PUDE``, ``XXX`` the access code series and
+    ``YYYYYYYYYY`` the real accesscode number.
+
+    This function requires a fully blown setup as it does catalog
+    lookups for finding applicants.
+    """
+    query = getUtility(IQuery)
+    results = list(query.searchResults(
+            Eq(('applicants_catalog', 'access_code'), identifier)
+            ))
+    if len(results) == 0:
+        return None
+    return results[0]
+
+def application_exists(identifier):
+    """Check whether an application for the given identifier already
+       exists.
+
+       `identifier` will normally be an access code.
+    """
+    applicant = get_applicant_data(identifier)
+    if applicant is None:
+        return False
+    return True
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/__init__.py	(revision 6703)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/sample_container_data.csv
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/sample_container_data.csv	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/sample_container_data.csv	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/applicants/tests/test_applicant.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_applicant.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_applicant.py	(revision 6703)
@@ -0,0 +1,201 @@
+##
+## test_applicant.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Aug 24 04:36:11 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 applicants and related.
+"""
+import unittest
+from StringIO import StringIO
+from hurry.file.interfaces import IFileRetrieval
+from zope.component import (
+    provideAdapter, adapts, getGlobalSiteManager, provideUtility)
+from zope.component.hooks import setSite
+from zope.component.interfaces import IFactory
+from zope.interface import verify, implements
+from zope.location.interfaces import ILocation
+from zope.publisher.base import TestRequest
+from zope.publisher.interfaces import NotFound
+from waeup.sirp.app import University
+from waeup.sirp.image import WAeUPImageFile, createWAeUPImageFile
+from waeup.sirp.image.interfaces import IWAeUPImageFile
+from waeup.sirp.applicants import (
+    ResultEntry, Applicant, ApplicantFactory, ApplicantTraverser,
+    )
+from waeup.sirp.applicants.interfaces import (
+    IResultEntry, IApplicant,
+    )
+from waeup.sirp.imagestorage import ImageStorageFileRetrieval
+from waeup.sirp.testing import (FunctionalTestCase, FunctionalLayer)
+
+class FakeImageLocation(object):
+    implements(ILocation)
+    adapts(IWAeUPImageFile)
+    def __init__(self, context):
+        pass
+
+class ResultEntryTest(unittest.TestCase):
+
+    def setUp(self):
+        self.result_entry = ResultEntry()
+        return
+
+    def tearDown(self):
+        pass
+
+    def test_interfaces(self):
+        verify.verifyClass(IResultEntry, ResultEntry)
+        verify.verifyObject(IResultEntry, self.result_entry)
+
+    def test_resultentry(self):
+        entry = ResultEntry('Some subject', 3.7)
+        assert entry.subject == 'Some subject'
+        assert entry.score == 3.7
+
+class ApplicantTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantTest, self).setUp()
+        self.applicant = Applicant()
+        return
+
+    def tearDown(self):
+        super(ApplicantTest, self).tearDown()
+        return
+
+    def test_interfaces(self):
+        verify.verifyClass(IApplicant, Applicant)
+        verify.verifyObject(IApplicant, self.applicant)
+        return
+
+    def test_passport_no_site(self):
+        # make sure we get a real image stored in passport attr
+        image = self.applicant.passport.file.read()
+        self.assertEqual(len(image), 2059)
+        return
+
+    def test_passport_insite(self):
+        # make sure we get a real image in passport attr when inside a site.
+        # When an applicant is created 'inside a site', its passport
+        # photograph should be put inside the images folder of the
+        # site, even if it is only a placeholder.
+        self.getRootFolder()['app'] = University()
+        app = self.getRootFolder()['app']
+        setSite(app)
+        applicant = Applicant()
+        image = self.applicant.passport.file.read()
+        self.assertEqual(len(image), 2059)
+        # The image contains a file_id instead of real image-data
+        self.assertEqual(self.applicant.passport.data,
+                         u'b48a1d39bbcb32e955d9ff2dea4ed0e6-1')
+        assert u'b48a1d39bbcb32e955d9ff2dea4ed0e6' in app['images'].keys()
+        return
+
+class ApplicantFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantFactoryTest, self).setUp()
+        # Install a IFileRetrieval utility that returns WAeUPImageFiles.
+        storage = ImageStorageFileRetrieval()
+        provideUtility(storage, IFileRetrieval)
+        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)
+
+
+class ApplicantTraverserTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantTraverserTest, self).setUp()
+        provideAdapter(FakeImageLocation)
+        self.applicant = Applicant()
+
+        self.request = TestRequest('')
+        return
+
+    def tearDown(self):
+        gsm = getGlobalSiteManager()
+        gsm.unregisterAdapter(FakeImageLocation)
+        super(ApplicantTraverserTest, self).tearDown()
+        return
+
+    def test_traverse_wo_passport(self):
+        # Ask for some attribute not provided
+        traverser = ApplicantTraverser(
+            self.applicant, self.request
+            )
+        self.assertRaises(
+            NotFound,
+            traverser.publishTraverse, self.request, 'passport'
+            )
+        return
+
+    def test_traverse_wo_image_passport_jpg(self):
+        # Ask for applicant pic if we didn't provided one
+        # We get a placeholder.
+        traverser = ApplicantTraverser(
+            self.applicant, self.request
+            )
+        result = traverser.publishTraverse(self.request, 'passport.jpg')
+        self.assertTrue(isinstance(result, FakeImageLocation))
+        return
+
+    def test_traverse_w_image_passport_jpg(self):
+        # Ask for applicant pic that's named 'passport.jpg'
+        traverser = ApplicantTraverser(
+            self.applicant, self.request
+            )
+        self.applicant.passport = createWAeUPImageFile(
+            'nofile.jpg', StringIO('no-content'))
+        self.applicant.passport.filename = 'mypic.jpg'
+        result = traverser.publishTraverse(self.request, 'passport.jpg')
+        self.assertTrue(isinstance(result, FakeImageLocation))
+        return
+
+    def test_traverse_w_image_some_jpg(self):
+        # Ask for applicant pic by correct name
+        traverser = ApplicantTraverser(
+            self.applicant, self.request
+            )
+        self.applicant.passport = WAeUPImageFile('nofile.jpg', '')
+        self.applicant.passport.filename = 'mypic.jpg'
+        result = traverser.publishTraverse(self.request, 'mypic.jpg')
+        self.assertTrue(isinstance(result, FakeImageLocation))
+        return
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_authentication.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_authentication.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_authentication.py	(revision 6703)
@@ -0,0 +1,383 @@
+##
+## test_authentication.py
+## Login : <uli@pu.smp.net>
+## Started on  Fri Aug 20 08:18:58 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
+##
+import grok
+import shutil
+import tempfile
+import unittest
+from zope.app.testing.functional import FunctionalTestCase
+from zope.authentication.interfaces import IAuthentication
+from zope.component import provideAdapter, getUtility
+from zope.component.hooks import setSite, clearSite
+from zope.interface import verify
+from zope.pluggableauth.interfaces import  IAuthenticatedPrincipalFactory
+from zope.publisher.browser import TestRequest
+from zope.publisher.interfaces import IRequest
+from zope.session.interfaces import ISession
+from zope.site import LocalSiteManager
+from zope.testbrowser.testing import Browser
+from zope.testing import cleanup
+from waeup.sirp.testing import FunctionalLayer
+from waeup.sirp.app import University
+from waeup.sirp.applicants import  ApplicantsContainer, Applicant
+from waeup.sirp.applicants.authentication import (
+    ApplicantsAuthenticatorPlugin, WAeUPApplicantCredentialsPlugin,
+    ApplicantCredentials, AuthenticatedApplicantPrincipalFactory,
+    ApplicantPrincipalInfo, ApplicantPrincipal,)
+
+
+class FakeBatch(dict):
+    def getAccessCode(self, id):
+        return self.get(id)
+
+class FakeAccessCode(object):
+    def __init__(self, repr, state = 'initialized'):
+        self.representation = repr
+        self.state = state
+
+class FakeApplication(object):
+    def __init__(self, ac=None):
+        self.access_code = ac
+
+def FakeSite():
+    return {
+        'applicants': {
+            'APP': {
+                'APP-12345': FakeApplication(u'APP-12345'),
+                'APP-54321': FakeApplication(u'APP-54321'),
+                'APP-22222': FakeApplication(u'APP-OTHER'),
+                'APP-44444': FakeApplication(),
+                'APP-55555': FakeApplication(u'APP-OTHER'),
+                'APP-77777': FakeApplication(u'APP-77777'),
+                },
+            },
+        'accesscodes': {
+            'APP': FakeBatch({
+                    'APP-12345': FakeAccessCode('APP-12345'),
+                    'APP-54321': FakeAccessCode('APP-54321', 'used'),
+                    'APP-11111': FakeAccessCode('APP-11111'),
+                    'APP-22222': FakeAccessCode('APP-22222'),
+                    'APP-33333': FakeAccessCode('APP-33333', 'used'),
+                    'APP-44444': FakeAccessCode('APP-44444', 'used'),
+                    'APP-55555': FakeAccessCode('APP-55555', 'used'),
+                    'APP-66666': FakeAccessCode('APP-66666', 'disabled'),
+                    'APP-77777': FakeAccessCode('APP-77777', 'disabled'),
+                    })
+            }
+        }
+
+class AuthenticatorPluginTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def create_applicant(self, cname, aname, ac=None):
+        # Create an applicant in an applicants container
+        setSite(self.app)
+        if not cname in self.app['applicants'].keys():
+            container = ApplicantsContainer()
+            self.app['applicants'][cname] = container
+        applicant = Applicant()
+        if ac is not None:
+            applicant.access_code = ac
+        self.app['applicants'][cname][aname] = applicant
+        return
+
+    def setUp(self):
+        super(AuthenticatorPluginTest, 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']
+
+        fake_site = FakeSite()
+        for ckey, fake_container in fake_site['applicants'].items():
+            for akey, fake_appl in fake_container.items():
+                self.create_applicant(ckey, akey, fake_appl.access_code)
+        del self.app['accesscodes']
+        self.app['accesscodes'] = fake_site['accesscodes']
+
+        self.plugin = ApplicantsAuthenticatorPlugin()
+        return
+
+    def tearDown(self):
+        super(AuthenticatorPluginTest, self).tearDown()
+        shutil.rmtree(self.dc_root)
+        return
+
+    def test_invalid_credentials(self):
+        result = self.plugin.authenticateCredentials('not-a-dict')
+        assert result is None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode=None, foo='blah'))
+        assert result is None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='Nonsense',))
+        assert result is None
+
+        # Possible cases, where formal correct authentication
+        # data is not valid:
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-33333'))
+        assert result is None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-55555'))
+        assert result is None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-66666'))
+        assert result is None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-77777'))
+        assert result is None
+
+        return
+
+    def test_valid_credentials(self):
+        """The six different cases where we allow login.
+
+        All other combinations should be forbidden.
+        """
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-11111'))
+        assert result is not None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-12345'))
+        assert result is not None
+
+        result = self.plugin.authenticateCredentials(
+            dict(accesscode='APP-54321'))
+        assert result is not None
+
+        # check the `principalInfo` method of authenticator
+        # plugin. This is only here to satisfy the coverage report.
+        assert self.plugin.principalInfo('not-an-id') is None
+        return
+
+session_data = {
+    'zope.pluggableauth.browserplugins': {}
+    }
+
+class FakeSession(dict):
+    def __init__(self, request):
+        pass
+
+    def get(self, key, default=None):
+        return self.__getitem__(key, default)
+
+    def __getitem__(self, key, default=None):
+        return session_data.get(key, default)
+
+    def __setitem__(self, key, value):
+        session_data[key] = value
+        return
+
+class CredentialsPluginTest(unittest.TestCase):
+
+    def setUp(self):
+        self.request = TestRequest()
+        provideAdapter(FakeSession, (IRequest,), ISession)
+        self.plugin = WAeUPApplicantCredentialsPlugin()
+        self.full_request = TestRequest()
+        session_data['zope.pluggableauth.browserplugins'] = {}
+        return
+
+    def tearDown(self):
+        cleanup.tearDown()
+        return
+
+    def filled_request(self, form_dict):
+        request = TestRequest()
+        for key, value in form_dict.items():
+            request.form[key] = value
+        return request
+
+    def test_extractCredentials_invalid(self):
+        result = self.plugin.extractCredentials('not-a-request')
+        assert result is None
+        return
+
+    def test_extractCredentials_empty(self):
+        result = self.plugin.extractCredentials(self.request)
+        assert result is None
+        return
+
+    def test_extractCredentials_full_set(self):
+        request = self.filled_request({
+                'form.ac_prefix': 'APP',
+                'form.ac_series': '1',
+                'form.ac_number': '1234567890',
+                #'form.jamb_reg_no': 'JAMB_NUMBER',
+                })
+        result = self.plugin.extractCredentials(request)
+        self.assertEqual(result, {'accesscode': 'APP-1-1234567890'})
+        return
+
+    def test_extractCredentials_accesscode_only(self):
+        request = self.filled_request({
+                'form.ac_prefix': 'APP',
+                'form.ac_series': '1',
+                'form.ac_number': '1234567890',
+                })
+        result = self.plugin.extractCredentials(request)
+        self.assertEqual(result, {'accesscode': 'APP-1-1234567890'})
+        return
+
+    def test_extractCredentials_from_empty_session(self):
+        session_data['zope.pluggableauth.browserplugins']['credentials'] = None
+        result = self.plugin.extractCredentials(self.request)
+        assert result is None
+        return
+
+    def test_extractCredentials_from_nonempty_session(self):
+        credentials = ApplicantCredentials('APP-1-12345')
+        session_data['zope.pluggableauth.browserplugins'][
+            'credentials'] = credentials
+        result = self.plugin.extractCredentials(self.request)
+        self.assertEqual(result, {'accesscode': 'APP-1-12345'})
+        return
+
+
+class ApplicantCredentialsTest(unittest.TestCase):
+
+    def setUp(self):
+        self.credentials = ApplicantCredentials('SOME_ACCESSCODE')
+        return
+
+    def tearDown(self):
+        return
+
+    def test_methods(self):
+        self.assertEqual(self.credentials.getAccessCode(), 'SOME_ACCESSCODE')
+        assert self.credentials.getLogin() is None
+        assert self.credentials.getPassword() is None
+        return
+
+class FakePluggableAuth(object):
+    prefix = 'foo'
+
+class PrincipalFactoryTest(unittest.TestCase):
+
+    def setUp(self):
+        self.info = ApplicantPrincipalInfo('APP-1-1234567890')
+        return
+
+    def tearDown(self):
+        pass
+
+    def test_principalFactory_interface(self):
+        verify.verifyClass(IAuthenticatedPrincipalFactory,
+                           AuthenticatedApplicantPrincipalFactory
+                           )
+        return
+
+    def test_principalFactory_create(self):
+        factory = AuthenticatedApplicantPrincipalFactory(self.info, None)
+
+        assert factory.info is self.info
+        assert factory.request is None
+        return
+
+    def test_principalFactory_call_w_prefix(self):
+        factory = AuthenticatedApplicantPrincipalFactory(self.info, None)
+        principal = factory(FakePluggableAuth())
+
+        assert isinstance(principal, ApplicantPrincipal)
+        self.assertEqual(principal.__repr__(),
+                         "ApplicantPrincipal('foo.APP-1-1234567890')")
+        self.assertEqual(principal.id, 'foo.APP-1-1234567890')
+        return
+
+    def test_principalFactory_call_wo_prefix(self):
+        factory = AuthenticatedApplicantPrincipalFactory(self.info, None)
+        fake_auth = FakePluggableAuth()
+        fake_auth.prefix = None
+        principal = factory(fake_auth)
+        self.assertEqual(principal.id, 'APP-1-1234567890')
+        return
+
+class PAUSetupTest(FunctionalTestCase):
+    # Check correct setup of authentication components in the
+    # applicants subpackage.
+
+    # When a university is created, we want by default have our local
+    # authentication components (an authenticator plugin and a
+    # credentials plugin) to be registered with the local PAU. Admins
+    # can remove these components on the fly later-on if they wish.
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(PAUSetupTest, 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(PAUSetupTest, self).tearDown()
+        shutil.rmtree(self.dc_root)
+
+    def test_extra_auth_plugins_installed(self):
+        # Check whether the auth plugins defined in here are setup
+        # automatically when a university is created
+
+        # Get the PAU responsible for the local site ('app')
+        pau = getUtility(IAuthentication, context=self.app)
+        cred_plugins = pau.getCredentialsPlugins()
+        auth_plugins = pau.getAuthenticatorPlugins()
+        cred_names = [name for name, plugin in cred_plugins]
+        auth_names = [name for name, plugin in auth_plugins]
+
+        # Make sure our local ApplicantsAuthenticatorPlugin is registered...
+        self.assertTrue('applicants' in auth_names)
+        # Make sure our local WAeUPApplicantCredentialsPlugin is registered...
+        self.assertTrue('applicant_credentials' in cred_names)
+        return
+
+def test_suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+        AuthenticatorPluginTest, CredentialsPluginTest,
+        ApplicantCredentialsTest, PrincipalFactoryTest,
+        PAUSetupTest,
+        ]:
+        suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+                testcase
+                )
+        )
+    return suite
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_batching.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_batching.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_batching.py	(revision 6703)
@@ -0,0 +1,138 @@
+##
+## test_batching.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Aug 24 02:04:44 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
+##
+"""Unit tests for applicants-related data importers.
+"""
+import grok
+import os
+import shutil
+import tempfile
+import unittest
+from zope.app.testing.functional import FunctionalTestCase
+from zope.component.hooks import setSite, clearSite
+from zope.interface.verify import verifyClass, verifyObject
+
+from waeup.sirp.app import University
+from waeup.sirp.applicants.batching import ApplicantsContainerImporter
+from waeup.sirp.applicants.container import ApplicantsContainer
+from waeup.sirp.applicants.interfaces import IApplicantsContainer
+from waeup.sirp.interfaces import DuplicationError
+from waeup.sirp.testing import FunctionalLayer, doctestsuite_for_module
+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(',')
+
+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))
+
+def test_suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+        ApplicantsContainerImporterTest,
+        ]:
+        suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+                testcase
+                )
+        )
+    return suite
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_browser.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_browser.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_browser.py	(revision 6703)
@@ -0,0 +1,871 @@
+##
+## test_browser.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Mar 29 11:31:11 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public 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
+from mechanize import LinkNotFoundError
+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 hurry.workflow.interfaces import IWorkflowInfo
+from waeup.sirp.testing import FunctionalLayer, FunctionalTestCase
+from waeup.sirp.app import University
+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
+
+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.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.ac_prefix = 'APP'
+        applicantscontainer.prefix = 'app'
+        applicantscontainer.year = 2009
+        applicantscontainer.application_category = 'basic'
+        self.app['applicants']['app2009'] = applicantscontainer
+
+        # Populate university
+        certificate = createObject('waeup.Certificate')
+        certificate.code = 'CERT1'
+        certificate.application_category = 'basic'
+        self.app['faculties']['fac1'] = Faculty()
+        self.app['faculties']['fac1']['dep1'] = Department()
+        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()
+        self.pin_applicant = unicode(self.pins[1])
+        self.applicant.access_code = self.pin_applicant
+        app['applicants']['app2009'][self.pin_applicant] = self.applicant
+
+    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)
+
+    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)
+
+    def test_manage_actions_access(self):
+        # Managers can access the action on manage screen
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_root_path)
+        self.browser.getControl("Add local role").click()
+        self.assertTrue('No user selected' in self.browser.contents)
+        return
+
+    def test_local_roles_add_delete(self):
+        # Managers can assign and delete local roles of applicants root
+        myusers = self.app['users']
+        myusers.addUser('bob', 'bobssecret')
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_root_path)
+        self.browser.getControl(name="user").value = ['bob']
+        self.browser.getControl(name="local_role").value = [
+            'waeup.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.ac_prefix").value = ['APP']
+        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.open(self.add_container_path)
+        self.browser.getControl("Cancel").click()
+        self.assertEqual(self.browser.url, self.manage_root_path + '#tab-1')
+        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()
+        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
+
+    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.ac_series").value = self.existing_series
+        self.browser.getControl(name="form.ac_number").value = self.existing_number
+        self.browser.getControl("Create application record").click()
+        self.assertTrue('Application initialized' in self.browser.contents)
+        self.browser.open(self.add_applicant_path)
+        self.browser.getControl(name="form.ac_series").value = '123'
+        self.browser.getControl(name="form.ac_number").value = '456'
+        self.browser.getControl("Create application record").click()
+        self.assertTrue('is not a valid access code' 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')
+        ctrl.getControl(value=self.existing_pin).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)
+        existing_pin = self.pins[2]
+        parts = existing_pin.split('-')[1:]
+        existing_series, existing_number = parts
+        self.browser.getControl(name="form.ac_series").value = existing_series
+        self.browser.getControl(name="form.ac_number").value = existing_number
+        self.browser.getControl("Create application record").click()
+        self.assertTrue('Application initialized' in self.browser.contents)
+        self.browser.open(self.container_manage_path)
+        self.assertTrue(existing_pin in self.browser.contents)
+        del self.app['applicants']['app2009'][existing_pin]
+        ctrl = self.browser.getControl(name='val_id')
+        ctrl.getControl(value=existing_pin).selected = True
+        self.browser.getControl("Remove selected", index=0).click()
+        self.assertMatches('...Could not delete...', self.browser.contents)
+        return
+
+    def test_manage_and_view_applicant(self):
+        # Managers can manage applicants
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.applicant_path = 'http://localhost/app/applicants/app2009/%s' % self.pin_applicant
+        self.applicant_view_path = self.applicant_path + '/index'
+        self.applicant_manage_path = self.applicant_path + '/edit_full'
+        self.applicant_slip_path = self.applicant_path + '/application_slip.pdf'
+        self.browser.open(self.applicant_manage_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getControl(name="form.email").value = 'abc'
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Invalid email address...', self.browser.contents)
+        self.browser.getControl(name="form.email").value = 'abc@def.gh'
+        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.course_admitted").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="transition").value = ['start']
+        self.browser.getControl("Save").click()
+        self.assertMatches('...Form has been saved...', self.browser.contents)
+        self.assertMatches('...Application started by zope.mgr...', self.browser.contents)
+        self.browser.open(self.applicant_view_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.open(self.applicant_slip_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        self.browser.open(self.applicant_manage_path)
+        self.browser.getControl(name="form.course_admitted").value = []
+        self.browser.getControl("Save").click()
+        self.browser.open(self.applicant_slip_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
+        return
+
+    def test_view_applicant(self):
+        # Applicants can login and view their application
+        self.login_path = 'http://localhost/app/applicants/app2009/login'
+        self.browser.open(self.login_path)
+        pin = self.pins[2]
+        parts = pin.split('-')[1:]
+        existing_series, existing_number = parts
+        ac_series = self.browser.getControl(name="form.ac_series")
+        ac_series.value = existing_series
+        ac_number = self.browser.getControl(name="form.ac_number")
+        ac_number.value = existing_number
+        self.browser.getControl(name="SUBMIT").click()
+        self.assertTrue(self.browser.url != self.login_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        return
+
+    def test_passport_edit_view(self):
+        # We get a default image after login
+        login_path = 'http://localhost/app/applicants/app2009/login'
+        self.browser.open(login_path)
+        pin = self.pins[2]
+        parts = pin.split('-')[1:]
+        existing_series, existing_number = parts
+        ac_series = self.browser.getControl(name="form.ac_series")
+        ac_series.value = existing_series
+        ac_number = self.browser.getControl(name="form.ac_number")
+        ac_number.value = existing_number
+        self.browser.getControl(name="SUBMIT").click()
+        pin = self.pins[2]
+        appl = self.getRootFolder()['app']['applicants']['app2009']
+        appl = appl[pin]
+        passp = appl.passport
+        #import pdb; pdb.set_trace()
+        passp_len = len(passp.file.read())
+        self.assertEqual(passp_len, PH_LEN)
+        #image_url = "%s/%s" % (self.browser.url, 'placeholder.jpg')
+        image_url = "%s/%s" % (self.browser.url, 'passport.jpg')
+        #self.browser.open(image_url)
+        self.browser.open('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.login_path = 'http://localhost/app/applicants/app2009/login'
+        self.browser.open(self.login_path)
+        pin = self.pins[2]
+        parts = pin.split('-')[1:]
+        existing_series, existing_number = parts
+        ac_series = self.browser.getControl(name="form.ac_series")
+        ac_series.value = existing_series
+        ac_number = self.browser.getControl(name="form.ac_number")
+        ac_number.value = existing_number
+        self.browser.getControl(name="SUBMIT").click()
+        self.assertTrue(self.browser.url != self.login_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getControl(name="form.firstname").value = 'John'
+        self.browser.getControl(name="form.lastname").value = 'Tester'
+        self.browser.getControl(name="form.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("Save").click()
+        self.assertMatches('...Form has been saved...', 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('Data 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_local_roles_add_delete(self):
+        # Managers can assign and delete local roles of applicants containers
+        myusers = self.app['users']
+        myusers.addUser('bob', 'bobssecret')
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.manage_container_path)
+        self.browser.getControl(name="user").value = ['bob']
+        self.browser.getControl(name="local_role").value = [
+            'waeup.ApplicationsOfficer']
+        self.browser.getControl("Add local role").click()
+        self.assertTrue('<td>bob</td>' in self.browser.contents)
+        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
+
+class LoginTest(FunctionalTestCase):
+    # Here we check login view of applicants containers.
+    #
+    # Tests in here do only cover login attempts without any PINs
+    # created before.
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(LoginTest, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+        self.login_path = 'http://localhost/app/applicants/testapplicants/login'
+
+        # Add an applicants container where we can login (or not)
+        applicantscontainer = ApplicantsContainer()
+        applicantscontainer.ac_prefix = 'APP'
+        app['applicants']['testapplicants'] = applicantscontainer
+
+        # Put the prepopulated site into test ZODB and prepare test
+        # browser
+        self.getRootFolder()['app'] = app
+        self.browser = Browser()
+        self.browser.handleErrors = False
+
+    def tearDown(self):
+        super(LoginTest, self).tearDown()
+        shutil.rmtree(self.dc_root)
+
+    def test_anonymous_access(self):
+        # Anonymous users can access a login page
+        self.browser.open(self.login_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        return
+
+    def test_anonymous_invalid_creds(self):
+        # Anonymous users giving invalid credentials stay at the page
+        self.browser.open(self.login_path)
+        # We do not give credentials but send the form as-is
+        submit = self.browser.getControl(name='SUBMIT')
+        submit.click()
+        # We are still at the same page...
+        self.assertEqual(self.browser.url, self.login_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        return
+
+    def test_anonymous_invalid_creds_warning(self):
+        # Entering wrong credentials will yield a warning
+        self.browser.open(self.login_path)
+        # We do not give credentials but send the form as-is
+        submit = self.browser.getControl(name='SUBMIT')
+        submit.click()
+        self.assertTrue(
+            'Entered credentials are invalid' in self.browser.contents)
+        return
+
+    def test_manager_no_warnings(self):
+        # Browsing the login screen as a manager, won't raise warnings
+        # Authenticate ourself as manager
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.login_path)
+        # Submit the form w/o any credentials
+        self.browser.getControl(name="SUBMIT").click()
+        self.assertTrue(
+            'Entered credentials are invalid' not in self.browser.contents)
+        return
+
+    def test_manager_no_redirect(self):
+        # Browsing the login screen as a manager won't trigger a redirect
+        # Authenticate ourself as manager
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.login_path)
+        # Submit the form w/o any credentials
+        self.browser.getControl(name="SUBMIT").click()
+        self.assertEqual(self.browser.url, self.login_path)
+        return
+
+    def test_display_entered_values(self):
+        # After submit the entered values are displayed in the form
+        self.browser.open(self.login_path)
+        # Enter some value we can look for after submit
+        ac_series = self.browser.getControl(name="form.ac_series")
+        ac_series.value = '666'
+        self.browser.getControl(name="SUBMIT").click()
+        self.assertTrue('666' in self.browser.contents)
+        return
+
+class LoginTestWithPINs(LoginTest):
+    # Here we check login view of applicants containers with PINs provided.
+
+    # As setting up pins is time-consuming we only set them up when
+    # really needed (i.e. in this separate TestCase).
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(LoginTestWithPINs, self).setUp()
+
+        # Create 5 access codes with prefix'FOO' and cost 9.99 each
+        pin_container = self.getRootFolder()['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
+        self.browser.handleErrors = False
+
+    def tearDown(self):
+        super(LoginTestWithPINs, self).tearDown()
+
+    def test_anonymous_valid_login(self):
+        # If we enter valid credentials, we get to the applicants form
+        self.browser.open(self.login_path)
+        # Enter some value we can look for after submit
+        ac_series = self.browser.getControl(name="form.ac_series")
+        ac_series.value = self.existing_series
+        ac_number = self.browser.getControl(name="form.ac_number")
+        ac_number.value = self.existing_number
+        self.browser.getControl(name="SUBMIT").click()
+        # We should be redirected to applicants form.
+        self.assertTrue(self.browser.url != self.login_path)
+        # Applicants see their Access Code in the contact form
+        self.browser.getLink("Contact").click()
+        self.assertTrue(
+            'Access Code:' in self.browser.contents)
+        return
+
+    def test_anonymous_invalid_login(self):
+        # If we enter wrong credentials we won't get far
+        self.browser.open(self.login_path)
+        # Enter some value we can look for after submit
+        ac_series = self.browser.getControl(name="form.ac_series")
+        ac_series.value = 'illegal series'
+        ac_number = self.browser.getControl(name="form.ac_number")
+        ac_number.value = 'invalid number'
+        self.browser.getControl(name="SUBMIT").click()
+        # We get a warning message
+        self.assertTrue(
+            'Entered credentials are invalid' in self.browser.contents)
+        # We stay at the login page (no redirect)
+        self.assertTrue(self.browser.url == self.login_path)
+        return
+
+class ApplicantsPassportTests(ApplicantsFullSetup):
+    # Tests for uploading/browsing the passport image of appplicants
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ApplicantsPassportTests, self).setUp()
+        self.login_path = 'http://localhost/app/applicants/app2009/login'
+        self.pin = self.pins[2]
+        self.existing_series, self.existing_number = self.pin.split('-')[1:]
+        self.edit_path = 'http://localhost/app/applicants/app2009/%s/edit' % (
+            self.pin)
+        self.edit_full_path = 'http://localhost/app/applicants/%s/%s/%s' % (
+            'app2009', self.pin, 'edit_full')
+
+    def tearDown(self):
+        super(ApplicantsPassportTests, self).tearDown()
+
+    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)
+        ac_series = self.browser.getControl(name="form.ac_series")
+        ac_series.value = self.existing_series
+        ac_number = self.browser.getControl(name="form.ac_number")
+        ac_number.value = self.existing_number
+        self.browser.getControl(name="SUBMIT").click()
+        self.applicant = self.app['applicants']['app2009'][self.pin]
+
+    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']
+
+    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.edit_path)
+        # There is a correct <img> link included
+        self.assertTrue(
+            '<img src="placeholder_m.jpg" />' in self.browser.contents)
+        # Browsing the link shows a real image
+        self.browser.open(self.image_url('placeholder_m.jpg'))
+        self.assertEqual(
+            self.browser.headers['content-type'], 'image/jpeg')
+        self.assertEqual(len(self.browser.contents), PH_LEN)
+
+    def test_after_login_default_stored_in_imagestorage(self):
+        # After login the applicants placeholder image is stored in
+        # an imagestorage
+        storage = self.app['images']
+        # In the beginning, the storage is empty
+        self.assertEqual([x for x in storage.keys()], [])
+        self.login()
+        # After login, it is filled
+        self.assertEqual(
+            [x for x in storage.keys()],
+            [u'b48a1d39bbcb32e955d9ff2dea4ed0e6'])
+        file_id = self.applicant.passport.data
+        self.assertEqual(
+            file_id, u'b48a1d39bbcb32e955d9ff2dea4ed0e6-1')
+        # The stored image can be fetched
+        fd = storage.retrieveFile(file_id)
+        file_len = len(fd.read())
+        self.assertEqual(file_len, PH_LEN)
+
+    def test_after_submit_default_browsable(self):
+        # After submitting an applicant form the default image is
+        # still visible
+        self.login()
+        self.browser.getControl("Save").click() # submit form
+        # There is a correct <img> link included
+        self.assertTrue(
+            '<img src="placeholder_m.jpg" />' in self.browser.contents)
+        # Browsing the link shows a real image
+        self.browser.open(self.image_url('placeholder_m.jpg'))
+        self.assertEqual(
+            self.browser.headers['content-type'], 'image/jpeg')
+        self.assertEqual(len(self.browser.contents), PH_LEN)
+
+    def test_after_submit_default_stored_in_imagestorage(self):
+        # After submitting an applicant form the default image is
+        # correctly stored in an imagestorage
+        self.login()
+        self.browser.getControl("Save").click() # submit form
+        storage = self.app['images']
+        self.assertEqual(
+            [x for x in storage.keys()],
+            [u'b48a1d39bbcb32e955d9ff2dea4ed0e6'])
+        file_id = self.applicant.passport.data
+        self.assertEqual(
+            file_id, u'b48a1d39bbcb32e955d9ff2dea4ed0e6-1')
+        # The stored image can be fetched
+        fd = storage.retrieveFile(file_id)
+        file_len = len(fd.read())
+        self.assertEqual(file_len, PH_LEN)
+
+    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()
+        # 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="myphoto.jpg" />' in self.browser.contents)
+        # Browsing the link shows a real image
+        self.browser.open(self.image_url('myphoto.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()
+        # 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 = self.app['images']
+        self.assertTrue(
+            u'18e57c7eac6ca7fb15b54b5b2bd4106d' in storage.keys())
+        # The stored image can be fetched
+        fd = storage.retrieveFile(u'18e57c7eac6ca7fb15b54b5b2bd4106d-1')
+        file_len = len(fd.read())
+        self.assertEqual(file_len, 31)
+        # The image uploaded belongs to the applicant
+        file_id = self.applicant.passport.data
+        self.assertEqual(
+            file_id, u'18e57c7eac6ca7fb15b54b5b2bd4106d-1')
+
+    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.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="myphoto.jpg" />' in self.browser.contents)
+        # Browsing the link shows a real image
+        self.browser.open(self.image_url('myphoto.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.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 = self.app['images']
+        self.assertTrue(
+            u'18e57c7eac6ca7fb15b54b5b2bd4106d' in storage.keys())
+        # The stored image can be fetched
+        fd = storage.retrieveFile(u'18e57c7eac6ca7fb15b54b5b2bd4106d-1')
+        #fd = storage.retrieveFile(file_id)
+        file_len = len(fd.read())
+        self.assertEqual(file_len, 31)
+        # The image uploaded belongs to the applicant
+        file_id = self.applicant.passport.data
+        self.assertEqual(
+            file_id, u'18e57c7eac6ca7fb15b54b5b2bd4106d-1')
+
+    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() # Create applicant form
+        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.edit_full_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')
+        passport0 = self.applicant.passport
+        self.browser.getControl("Save").click() # submit form with changed pic
+        passport1 = self.applicant.passport
+        self.browser.getControl("Save").click() # submit form w/o changes
+        passport2 = self.applicant.passport
+        self.assertTrue(passport0 != passport1)
+        self.assertTrue(passport1 == passport2)
+        self.assertTrue(passport1 is passport2)
+        return
+
+    def test_final_submit(self):
+        # Make sure that a correctly filled form with passport picture
+        # can be submitted
+        self.login() # Create applicant form
+        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("Final Submit").click() # (finally) submit form
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertTrue(
+            'Passport confirmation box not ticked' in self.browser.contents)
+        self.browser.getControl(name="confirm_passport").value = True
+        IWorkflowInfo(self.applicant).fireTransition('submit')
+        self.browser.getControl("Final Submit").click() # submit form again
+        self.assertTrue(
+            'Wrong state' in self.browser.contents)
+        IWorkflowInfo(self.applicant).fireTransition('reset1')
+        # Now do the whole thing again but with correct state
+        self.login() 
+        self.fill_correct_values() 
+        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(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)
+        #import pdb; pdb.set_trace()
+        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() # Create applicant form
+        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()
+        self.browser.getLink("Logout").click()
+
+        # Login as manager and lock the form
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.edit_full_path)
+        self.browser.getControl(name="form.locked").value = True
+        self.browser.getControl("Save").click()
+        self.browser.getLink("Logout").click()
+
+        # Login as applicant again and try to open the edit form
+        self.login()
+        self.browser.open(self.edit_path)
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        #print self.browser.contents
+        self.assertTrue(
+            'The requested form is locked' in self.browser.contents)
+        return
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_catalog.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_catalog.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_catalog.py	(revision 6703)
@@ -0,0 +1,94 @@
+##
+## test_catalog.py
+## Login : <uli@pu.smp.net>
+## Started on  Mon May 16 16:15: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
+##
+"""
+Tests for applicants specific catalog (currently only one).
+"""
+import shutil
+import tempfile
+import unittest
+from hurry.query import Eq
+from hurry.query.interfaces import IQuery
+from zope.app.testing.functional import FunctionalTestCase
+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
+
+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()
+        setSite(self.app)
+        self.app['applicants']['mystuff'] = self.container
+        self.applicant = Applicant()
+        self.ac = u'FOO-666-123456789'
+        self.applicant.access_code = self.ac
+        self.app['applicants']['mystuff'][self.ac] = 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', 'access_code'), self.ac)
+        results = list(q.searchResults(subquery))
+        self.assertEqual(len(results), 1)
+
+        result_applicant = results[0]
+        self.assertTrue(isinstance(result_applicant, Applicant))
+        self.assertEqual(result_applicant.access_code, self.ac)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_container.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_container.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_container.py	(revision 6703)
@@ -0,0 +1,63 @@
+##
+## test_container.py
+## Login : <uli@pu.smp.net>
+## Started on  Thu Jan 20 09:30:37 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
+##
+"""
+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/uli-studentpw/src/waeup/sirp/applicants/tests/test_interfaces.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_interfaces.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_interfaces.py	(revision 6703)
@@ -0,0 +1,101 @@
+##
+## test_interfaces.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Jan 25 17:09:46 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 components defined in interface module.
+"""
+import unittest
+from zc.sourcefactory.browser.source import FactoredTerms
+from zope.browser.interfaces import ITerms
+from zope.component import getMultiAdapter
+from zope.interface.verify import verifyClass, verifyObject
+from zope.publisher.browser import TestRequest
+from waeup.sirp.applicants import interfaces
+from waeup.sirp.applicants.vocabularies import (
+    APPLICATION_TYPES, application_types_vocab, GenderSource,
+    )
+
+class ApplicationCategoriesTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.vocab = application_types_vocab
+        return
+
+    def test_vocabulary_len(self):
+        self.assertEqual(len(self.vocab), 10)
+        return
+
+    def test_vocabulary_items(self):
+        self.assertTrue('pude' in self.vocab)
+        return
+
+    def test_term_attributes(self):
+        # Check that each vocab entry provided value, token, and title.
+        term = self.vocab.getTermByToken('pude')
+        self.assertEqual(term.token, 'pude')
+        self.assertEqual(term.value, 'pude')
+        self.assertEqual(term.title, 'Post UDE Screening')
+        return
+
+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 [
+            ApplicationCategoriesTestCase,
+            InterfacesTest,
+            ]:
+        suite.addTests(unittest.TestLoader().loadTestsFromTestCase(testcase))
+    return suite
+
+test_suite = suite
+
+
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_root.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_root.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/tests/test_root.py	(revision 6703)
@@ -0,0 +1,262 @@
+##
+## test_root.py
+## Login : <uli@pu.smp.net>
+## Started on  Thu Jan 20 09:26:54 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 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, application_exists, get_applicant_data, Applicant,
+    ApplicantsContainer,
+    )
+from waeup.sirp.applicants.root import (
+    ApplicantsRoot, ApplicantsPlugin,
+    )
+from waeup.sirp.testing import (
+    FunctionalLayer, FunctionalTestCase, get_all_loggers, remove_new_loggers,
+    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
+
+
+class HelperToolsTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(HelperToolsTest, 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)
+
+        # Insert some applicants containers and applicants...
+        self.container1 = ApplicantsContainer()
+        self.app['applicants']['foo'] = self.container1
+        self.container2 = ApplicantsContainer()
+        self.app['applicants']['bar'] = self.container2
+        self.ac = u'APP-99999'
+        self.applicant = Applicant()
+        self.applicant.access_code = self.ac
+        self.app['applicants']['foo'][self.ac] = self.applicant
+        return
+
+    def tearDown(self):
+        super(HelperToolsTest, self).tearDown()
+        shutil.rmtree(self.dc_root)
+        return
+
+    def test_application_exists(self):
+        result = application_exists('APP-99999')
+        assert result is True
+
+        result = application_exists('APP-44444')
+        assert result is False
+        return
+
+    def test_get_applicant_data(self):
+        result = get_applicant_data('APP-99999')
+        self.assertEqual(result, self.applicant)
+        return
+
+    def test_get_applicant_data_none(self):
+        result = get_applicant_data('NOT-EXISTIING')
+        assert result is None
+        return
+
+def suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+            ApplicantsRootTestCase,
+            ApplicantsRootPluginTestCase,
+            HelperToolsTest,
+            ]:
+        suite.addTests(unittest.makeSuite(testcase))
+    return suite
+
+test_suite = suite
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/vocabularies.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/vocabularies.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/vocabularies.py	(revision 6703)
@@ -0,0 +1,46 @@
+"""Vocabularies and sources for the application section.
+"""
+from datetime import datetime
+from zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from zc.sourcefactory.basic import BasicSourceFactory
+from zc.sourcefactory.contextual import BasicContextualSourceFactory
+from waeup.sirp.interfaces import SimpleWAeUPVocabulary
+from waeup.sirp.students.lgas import LGAS
+from waeup.sirp.students.vocabularies import (
+    entry_session_vocab, CertificateSource, GenderSource)
+
+#: Types of applications we support.
+APPLICATION_TYPES = (
+    ('General Studies', 'app','APP'),
+    ('Pre-NCE Programme', 'prence','PRE'),
+    ('Post UME Screening Test', 'pume','PUME'),
+    ('Post UDE Screening', 'pude','PUDE'),
+    ('Part Time Degree in Education', 'sandwich','SAND'),
+    ('Part-Time Degree Programmes', 'pt','PTP'),
+    ('Diploma Programmes', 'dp','DPP'),
+    ('PCE Screening', 'pce','PCE'),
+    ('Certificate Programmes', 'ct','CTP'),
+    ('Common Entry Screening Test', 'cest','CEST'),
+    )
+
+#: A :class:`waeup.sirp.interfaces.SimpleWAeUPVocabulary` of supported
+#: application or screening types.
+application_types_vocab = SimpleWAeUPVocabulary(
+    *[(x[0],x[1]) for x in APPLICATION_TYPES])
+application_pins_vocab = SimpleWAeUPVocabulary(
+    *[(u"%s (%s)" % (x[2],x[0]),x[2]) for x in APPLICATION_TYPES])
+
+
+class AppCatCertificateSource(CertificateSource):
+    """An application certificate source delivers all courses which belong to
+    a certain application_category.
+    """
+    def getValues(self, context):
+        appcat = context.__parent__.application_category
+        catalog = getUtility(ICatalog, name='certificates_catalog')
+        return sorted(list(
+                catalog.searchResults(
+                    code=('', 'z*'),
+                    application_category=(appcat,appcat))),
+                    key=lambda value: value.code)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/workflow.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/workflow.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/applicants/workflow.py	(revision 6703)
@@ -0,0 +1,134 @@
+"""Workflow for applicants.
+"""
+import grok
+from datetime import datetime
+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, IWAeUPWorkflowInfo
+from waeup.sirp.workflow import WAeUPWorkflow, WAeUPWorkflowInfo
+from waeup.sirp.utils.helpers import get_current_principal
+
+INITIALIZED = 'initialized'
+STARTED = 'started'
+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 = 'submit',
+        title = 'Submit application',
+        msg = 'Application submitted',
+        source = STARTED,
+        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',
+        msg = 'Application reset',
+        source = SUBMITTED,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset2',
+        title = 'Reset application',
+        msg = 'Application reset',
+        source = ADMITTED,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset3',
+        title = 'Reset application',
+        msg = 'Application reset',
+        source = NOT_ADMITTED,
+        destination = STARTED),
+
+    Transition(
+        transition_id = 'reset4',
+        title = 'Reset application',
+        msg = 'Application reset',
+        source = CREATED,
+        destination = STARTED),
+    )
+
+application_workflow = WAeUPWorkflow(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(WAeUPWorkflowInfo, grok.Adapter):
+    """Adapter to adapt Applicant objects to workflow info objects.
+    """
+    grok.context(IApplicantBaseData)
+    grok.provides(IWAeUPWorkflowInfo)
+
+    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 = '%s' % 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:
+        user = get_current_principal()
+        applicants_root = grok.getSite()['applicants']
+        applicants_root.logger.info('%s - %s - %s' % (user.id,obj.access_code,msg))
+    except (TypeError, AttributeError):
+        pass
+    return
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/authentication.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/authentication.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/authentication.py	(revision 6703)
@@ -0,0 +1,187 @@
+"""Authentication for WAeUP portals.
+"""
+import grok
+from zope.component import getUtility, getUtilitiesFor
+from zope.pluggableauth.plugins.session import SessionCredentialsPlugin
+from zope.pluggableauth.interfaces import (
+        ICredentialsPlugin, IAuthenticatorPlugin, IPrincipalInfo)
+from zope.password.interfaces import IPasswordManager
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.securitypolicy.principalrole import principalRoleManager
+from waeup.sirp.interfaces import IUserAccount, IAuthPluginUtility
+
+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 WAeUPSessionCredentialsPlugin(grok.GlobalUtility,
+                                    SessionCredentialsPlugin):
+    grok.provides(ICredentialsPlugin)
+    grok.name('credentials')
+
+    loginpagename = 'login'
+    loginfield = 'form.login'
+    passwordfield = 'form.password'
+
+class PrincipalInfo(object):
+    grok.implements(IPrincipalInfo)
+
+    def __init__(self, id, title, description):
+        self.id = id
+        self.title = title
+        self.description = description
+        self.credentialsPlugin = None
+        self.authenticatorPlugin = None
+
+class Account(grok.Model):
+    grok.implements(IUserAccount)
+
+    _local_roles = dict()
+
+    def __init__(self, name, password, title=None, description=None,
+                 roles = []):
+        self.name = name
+        if title is None:
+            title = name
+        if description is None:
+            description = title
+        self.title = title
+        self.description = description
+        self.setPassword(password)
+        self.setRoles(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 getRoles(self):
+        prm = get_principal_role_manager()
+        roles = [x[0] for x in prm.getRolesForPrincipal(self.name)
+                 if x[0].startswith('waeup.')]
+        return roles
+
+    def setRoles(self, roles):
+        prm = get_principal_role_manager()
+
+        old_roles = self.getRoles()
+        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)
+
+    roles = property(getRoles, setRoles)
+
+    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 PrincipalInfo(id=account.name,
+                             title=account.title,
+                             description=account.description)
+
+    def principalInfo(self, id):
+        account = self.getAccount(id)
+        if account is None:
+            return None
+        return PrincipalInfo(id=account.name,
+                             title=account.title,
+                             description=account.description)
+
+    def getAccount(self, login):
+        # ... look up the account object and return it ...
+        usercontainer = self.getUserContainer()
+        if usercontainer is None:
+            return
+        return usercontainer.get(login, None)
+
+    def addAccount(self, account):
+        usercontainer = self.getUserContainer()
+        if usercontainer is None:
+            return
+        # XXX: complain if name already exists...
+        usercontainer.addAccount(account)
+
+    def addUser(self, name, password, title=None, description=None):
+        usercontainer = self.getUserContainer()
+        if usercontainer is None:
+            return
+        usercontainer.addUser(name, password, title, description)
+
+    def getUserContainer(self):
+        site = grok.getSite()
+        return site['users']
+
+@grok.subscribe(IUserAccount, grok.IObjectRemovedEvent)
+def handle_account_removal(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
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/authentication.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/authentication.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/authentication.txt	(revision 6703)
@@ -0,0 +1,205 @@
+WAeUP portal authentication
+***************************
+
+:Test-Layer: functional
+
+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')
+  >>> root['app']['users'].addAccount(alice)
+
+See ``users.txt`` for details about the UserContainer we use here.
+
+Users and local roles
+=====================
+
+Accounts also hold infos about local roles assigned to a user. In the
+beginning, users have no local roles at all:
+
+  >>> alice.getLocalRoles()
+  {}
+
+But we can tell an account, that Alice got some role for a certain
+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()
+  {}
+
+We can also use events to trigger such actions. This is recommended
+because we do not neccessarily know where Alice lives:
+
+  >>> from waeup.sirp.users 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()
+   {}
+
+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/uli-studentpw/src/waeup/sirp/browser/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/__init__.py	(revision 6703)
@@ -0,0 +1,3 @@
+from waeup.sirp.browser.layout import (
+    WAeUPPage, WAeUPLayout, WAeUPDisplayFormPage, WAeUPEditFormPage,
+    WAeUPAddFormPage, NullValidator)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/batchprocessing.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/batchprocessing.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/batchprocessing.txt	(revision 6703)
@@ -0,0 +1,573 @@
+Batch processing via browser
+****************************
+
+:test-layer: functional
+
+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,review_state,title,title_prefix
+    ... FAC1,static,Faculty 1,faculty
+    ... FAC2,static,Faculty 2,institute
+    ... FAC3,static,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
+    ['Applicants Container 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 watch the entries generated in logfile:
+
+    >>> browser.getControl('View processing log').click()
+    >>> print browser.contents
+    <!DOCTYPE ...
+    <pre>...
+ 
+    ... - INFO - --------------------
+    ... - INFO - zope.mgr: Batch processing finished: OK
+    ... - INFO - zope.mgr: Source: /.../faculties_zope.mgr.csv
+    ... - INFO - zope.mgr: Mode: create
+    ... - INFO - zope.mgr: User: zope.mgr
+    ... - INFO - zope.mgr: Processing time: ... s (... s/item)
+    ... - INFO - zope.mgr: Processed: 3 lines (3 successful/ 0 failed)
+    ... - INFO - --------------------
+    </pre>
+    ....
+
+
+Batch processing departments
+============================
+
+    >>> browser.open('http://localhost/app/datacenter')
+
+Prepare a CSV file for departments:
+
+    >>> open('departments.csv', 'wb').write(
+    ... """code,faculty_code,review_state,title,title_prefix
+    ... DEP1,FAC1,static,Department 1,department
+    ... DEP2,FAC2,static,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=1).selected = True
+    >>> modeselect = browser.getControl(name='mode')
+    >>> modeselect.getControl(value='create').selected = True
+    >>> browser.getControl('Proceed to step 3...').click()
+
+Step 3: Fix headerlines
+
+We get informed that there are no problems with the current header:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...
+    Header fields OK
+    ...
+
+The submit button is enabled:
+
+    >>> browser.getControl('Perform import...').disabled
+    False
+
+    >>> browser.getControl('Perform import...').click()
+
+Step 4: See import results
+
+The import was successful:
+
+    >>> print browser.contents
+    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    ...Successfully processed 2 rows...
+    ...Batch processing finished...
+    ...File:...courses_zope.mgr.csv...
+
+Batch processing certificates
+=============================
+
+    >>> browser.open('http://localhost/app/datacenter')
+
+Prepare a CSV file for certificates:
+
+    >>> open('certificates.csv', 'wb').write(
+    ... """code,faculty_code,department_code,title,study_mode,start_level,end_level,application_category
+    ... CERT1,FAC1,DEP1,Certificate 1,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,core_or_elective
+    ... 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,review_state,title,title_prefix
+    ... FAC1,static,Faculty 1,faculty
+    ... FAC4,static,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,--IGNORE--,code,title,--ERRORS--
+    faculty,static,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,--IGNORE--,code,title
+    school,static,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,static,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=1).click()
+    >>> '<input type="submit" name="back" value="Back" />' in browser.contents
+    True
+    >>> 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/uli-studentpw/src/waeup/sirp/browser/breadcrumbs.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/breadcrumbs.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/breadcrumbs.py	(revision 6703)
@@ -0,0 +1,218 @@
+""" 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.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.IWAeUPObject)
+    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
+
+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 FacultyContainerBreadcrumb(Breadcrumb):
+    """A breadcrumb for faculty containers.
+    """
+    grok.context(interfaces.IFacultyContainer)
+    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 UserContainerBreadcrumb(Breadcrumb):
+    """A breadcrumb for user containers.
+    """
+    grok.context(interfaces.IUserContainer)
+    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 CourseContainerBreadcrumb(Breadcrumb):
+    """ We don't want course container breadcrumbs.
+    """
+    grok.context(interfaces.ICourseContainer)
+    grok.implements(IBreadcrumbIgnorable)
+
+class CertificateContainerBreadcrumb(Breadcrumb):
+    """ We don't want course container breadcrumbs.
+    """
+    grok.context(interfaces.ICertificateContainer)
+    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/uli-studentpw/src/waeup/sirp/browser/breadcrumbs.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/breadcrumbs.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/breadcrumbs.txt	(revision 6703)
@@ -0,0 +1,245 @@
+Breadcrumbs
+***********
+
+:test-layer: functional
+
+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.UserContainerBreadcrumb 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/uli-studentpw/src/waeup/sirp/browser/browser.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/browser.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/browser.txt	(revision 6703)
@@ -0,0 +1,1198 @@
+Browsing the WAeUP portal
+*************************
+
+Here we visit all parts of a WAeUP portal using a browser.
+
+:Test-Layer: functional
+
+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 the...
+  ...
+
+The contact form for anonymous users is called 'Enquiries'::
+
+  >>> browser.open('http://localhost/myuniversity')
+  >>> browser.getLink('Enquiries').click()
+  >>> browser.getControl('Submit').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...Error: All fields must be filled...
+  ...
+
+  >>> browser.getControl(name='fullname').value = "Bob Tester"
+  >>> browser.getControl(name='email').value = "xx@yy.zz"
+  >>> browser.getControl(name='descr').value = "test message"
+  >>> browser.getControl('Submit').click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<li class="message">Your message has been sent.</li>
+  ...
+
+Now we login as manager::
+
+  >>> browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+  >>> browser.handleErrors = False
+
+We can then get an edit view of a university::
+
+  >>> browser.open('http://localhost/myuniversity/manage')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<form action="http://localhost/myuniversity/manage"
+  ...
+
+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::
+
+  >>> 'waeuptheme-gray1.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"...
+  ...<li class="message">Settings have been saved.</li>
+  ...
+
+  >>> browser.getControl("Cancel").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<li class="message">Action cancelled.</li>
+  ...
+
+  >>> browser.open('http://localhost/myuniversity/manage')
+  >>> browser.getControl("Save and return").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<li class="message">Settings have been saved.</li>
+  ...
+
+  >>> browser.open('http://localhost/myuniversity/manage')
+  >>> browser.getControl("Update plugins").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<li class="message">Plugins were updated. See log file for details.</li>
+  ...
+
+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"...
+  ...manage portal users...
+  ...
+
+We can add users:
+
+  >>> browser.getLink("Add user").click()
+  >>> browser.getControl(name="form.name").value = 'bob'
+  >>> browser.getControl(name="form.password").value = 'secret'
+  >>> 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("edit", index=0).click()
+  >>> browser.getControl("Save", index=0).click()
+  >>> browser.getControl("Save and return", index=0).click()
+  >>> browser.getControl("edit", index=0).click()
+  >>> browser.getControl("Cancel", index=0).click()
+
+We can add global roles which are then displayed on the user container page.
+Since the test browser does not use javascript, we have to add global roles
+manually by setting the roles attribute:
+
+  >>> print root['myuniversity']['users']['bob'].roles
+  []
+  >>> root['myuniversity']['users']['bob'].roles = ['waeup.PortalUser']
+  >>> print root['myuniversity']['users']['bob'].roles
+  ['waeup.PortalUser']
+  >>> browser.open('http://localhost/myuniversity/users')
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<td>Portal User <br /></td>
+  ...
+
+Users can't be added twice:
+
+  >>> browser.open('http://localhost/myuniversity/users/add')
+  >>> browser.getControl(name="form.name").value = 'bob'
+  >>> browser.getControl(name="form.password").value = 'secret'
+  >>> 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("delete").click()
+  >>> 'User 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 in a wrong email address::
+
+  >>> browser.getControl(name='email').value = "xx@yy"
+  >>> browser.getControl(name='descr').value = "test message"
+  >>> browser.getControl('Submit').click()
+  >>> print browser.contents      
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<li class="message">Error: xx@yy is not a valid email address.</li>
+  ...
+
+Now we fill the form properly (this will send a real message to
+contact@waeup.org)::
+
+  >>> browser.open('http://localhost/myuniversity/contactadmin')
+  >>> browser.getControl(name='email').value = "xx@yy.zz"
+  >>> browser.getControl(name='descr').value = "test message"
+  >>> browser.getControl('Submit').click()
+  >>> print browser.contents      
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...<li class="message">Your message has been sent.</li>
+  ...
+
+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 class="required">*</span>
+  ...<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"
+  ...<h2>Academic Section</h2>
+  ...<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"...
+  ...<h2>Academic Section</h2>
+  ...<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.DepartmentOfficer']
+  >>> 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 Officer:...
+  ...
+
+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.DepartmentOfficer').selected = True
+  >>> browser.getControl("Remove selected local roles").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...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 faculty 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 class="required">*</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"...
+  ...<li class="message">The code chosen already exists in this faculty.</li>
+  ...
+
+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"...
+  ...<h2>Departments</h2>
+  ...<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"
+  ...<h2>Departments</h2>
+  ...<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.DepartmentOfficer']
+  >>> 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.DepartmentOfficer').selected = True
+  >>> browser.getControl("Remove selected local roles").click()
+  >>> print browser.contents
+  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"...
+  ...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 department 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:`ICourseContainer` instances with their
+code as key. CourseContainers 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
+coursecontainers (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 class="required">*</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
+  ['0', '1', '2', '3']
+
+  >>> ctrl.displayOptions
+  ['N/A', 'First Semester', 'Second Semester', 'Combined']
+
+  >>> 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"...
+  ...<li class="message">A course with same code already exists:...</li>
+  ...
+
+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. This time we will return to the overview page
+afterwards:
+
+  >>> browser.getControl(name='form.title').value = "My renamed course" 
+  >>> ctrl = browser.getControl("Save and return")
+  >>> ctrl.click()
+
+If we go to the settings page and click ``Cancel`` nothing will be
+changed:
+
+  >>> browser.getLink('Edit course').click()
+  >>> browser.getControl(name='form.title').value = "Blah"
+  >>> browser.getControl('Cancel').click()
+
+Our course was indeed renamed to ``My renamed course`` 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 course</td>...
+  ...
+
+Searching courses
+-----------------
+
+  >>> browser.open('http://localhost/myuniversity/faculties/search')
+  >>> browser.getControl(name='query').value = "My renamed 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.getControl(name='query').value = "Nonsense"
+  >>> browser.getControl('Search').click()
+  >>> 'Search Results' in browser.contents
+  False
+
+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 renamed course' in browser.contents
+  True
+
+  >>> browser.getControl('Cancel', index=0).click()
+  >>> browser.getLink('Manage department').click()
+  >>> browser.getControl('Remove selected courses').click()
+  >>> 'No course 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
+
+
+Certificates
+============
+
+Once we have a department, we can add also certificats.
+
+Adding certificates
+-------------------
+
+Certificates are stored in :class:`ICertificateContainer` instances
+with their code as key. CertificateContainers 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
+certificatecontainers (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 class="required">*</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"...
+  ...<li class="message">A certificate with same code already exists:...</li>
+  ...
+
+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>...
+  ...
+
+
+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 certificate 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 class="required">*</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>
+  ...
+
+
+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 course referrer 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>/.../waeup/sirp/files</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,core_or_elective
+  ... 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/uli-studentpw/src/waeup/sirp/browser/exceptions.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/exceptions.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/exceptions.py	(revision 6703)
@@ -0,0 +1,84 @@
+##
+## exceptions.py
+## Login : <uli@pu.smp.net>
+## Started on  Sun May 22 18:28:46 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
+##
+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 WAeUPPage
+
+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(WAeUPPage):
+    """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/uli-studentpw/src/waeup/sirp/browser/interfaces.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/interfaces.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/interfaces.py	(revision 6703)
@@ -0,0 +1,90 @@
+"""Interfaces for viewing components.
+"""
+from zope import schema
+from zope.interface import Interface, Attribute
+from waeup.sirp.interfaces import (
+    IWAeUPObject, IUniversity, IUserContainer, IDataCenter)
+from waeup.sirp.university.interfaces import (
+    IFacultyContainer, IFaculty, IFacultyAdd, IDepartment, IDepartmentAdd, 
+    ICourseContainer, ICourse, ICourseAdd, ICertificateContainer, 
+    ICertificate, ICertificateAdd, ICertificateCourse, ICertificateCourseAdd)
+class IBreadcrumb(Interface):
+    """Provide breadcrumbs.
+    """
+    title = schema.TextLine(
+        title = u'Context description',
+        readonly = True,
+        )
+    viewname = schema.TextLine(
+        title = u'The viewname, for which the breadcrumb is provided.',
+        readonly = True,
+        default = u'index'
+        )
+    parent_viewname = schema.TextLine(
+        title = u'The viewname of the parent to use.',
+        readonly = True,
+        default = u'index'
+        )
+    parent = Attribute("The parent object.")
+    context = Attribute("The context of the breadcrumb.")
+
+
+    def getURL(view=None):
+        """Return a URL to the breadcrumb 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.
+        """
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/layout.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/layout.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/layout.py	(revision 6703)
@@ -0,0 +1,196 @@
+"""Basic layout components.
+"""
+import os
+import grok
+import grokcore.view
+from hurry.resource import mode
+from megrok.layout import Page, Layout, 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 IWAeUPObject, IUserAccount
+from waeup.sirp.browser.interfaces import ITheme
+from waeup.sirp.browser.theming import get_all_themes, WAeUPThemeGray1
+from waeup.sirp.students.interfaces import IStudentNavigation
+from waeup.sirp.authentication import get_principal_role_manager
+
+grok.templatedir('templates')
+default_waeup_display_template = grokcore.view.PageTemplateFile(
+    os.path.join('templates', 'default_waeup_display_form.pt'))
+default_waeup_display_template.__grok_name__ = 'default_waeup_display_form'
+
+default_waeup_edit_template = grokcore.view.PageTemplateFile(
+    os.path.join('templates', 'default_waeup_edit_form.pt'))
+default_waeup_edit_template.__grok_name__ = 'default_waeup_edit_form'
+
+
+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='message'):
+        """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 WAeUPLayout(Layout, UtilityView):
+    """A megrok.layout.Layout with additional methods.
+    """
+    grok.baseclass()
+
+class WAeUPPage(Page):
+    """A megrok.layout page with additional methods.
+    """
+    grok.baseclass()
+
+class WAeUPDisplayFormPage(DisplayForm, UtilityView):
+    """A megrok.layout.DisplayForm with additional methods.
+    """
+    grok.baseclass()
+    template = default_waeup_display_template
+
+class WAeUPEditFormPage(EditForm, UtilityView):
+    """A megrok.layout.EditForm with additional methods.
+    """
+    grok.baseclass()
+    template = default_waeup_edit_template
+
+    def setUpWidgets(self,ignore_request=False):
+        super(WAeUPEditFormPage,self).setUpWidgets(ignore_request)
+        if self.widgets.get('title'):
+            self.widgets['title'].displayWidth = 80
+        if self.widgets.get('frontpage'):
+            self.widgets['frontpage'].width = 70
+        if self.widgets.get('phone'):
+            self.widgets['phone'].displayWidth = 13
+        if self.widgets.get('notice'):
+            self.widgets['notice'].height = 3
+
+class WAeUPAddFormPage(AddForm,  UtilityView):
+    """A megrok.layout.AddForm with additional methods.
+    """
+    grok.baseclass()
+    grok.template('default_waeup_edit_form')
+
+class SiteLayout(WAeUPLayout):
+    """ The general site layout.
+    """
+    grok.context(IWAeUPObject)
+
+    #: An instance of the default theme to use for the site layout
+    default_theme = WAeUPThemeGray1()
+    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(), 'name', u'Sample University')
+
+    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'
+        elif usertitle == 'Applicant':
+            return self.request.principal.id
+        return usertitle
+
+    def getUserId(self):
+        """Return id of current user.
+        """
+        userid = self.request.principal.id
+        return userid
+
+    def getStudentName(self):
+        """Return the student name.
+        """
+        if IStudentNavigation.providedBy(self.context):
+            return self.context.getStudent().name
+        return
+
+    def isStudent(self):
+        prm = get_principal_role_manager()
+        roles = [x[0] for x in prm.getRolesForPrincipal(self.request.principal.id)]
+        return 'waeup.Student' in roles
+
+    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(), '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 not self.isAuthenticated():
+            return self.studenttemp.render(self)
+        return self.stafftemp.render(self)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/pages.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/pages.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/pages.py	(revision 6703)
@@ -0,0 +1,1592 @@
+# -*- coding: utf-8 -*-
+""" Viewing components for WAeUP objects.
+"""
+import copy
+import csv
+import grok
+import os
+import re
+import sys
+import time
+import re
+import smtplib
+from email.mime.text import MIMEText
+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, createObject,getAllUtilitiesRegisteredFor)
+#from zope.component.interfaces import Invalid
+from zope.event import notify
+from zope.session.interfaces import ISession
+from waeup.sirp.browser import (
+    WAeUPPage, WAeUPEditFormPage, WAeUPAddFormPage,
+    WAeUPDisplayFormPage, NullValidator)
+from waeup.sirp.browser.interfaces import (
+    IUniversity, IFacultyContainer, IFaculty, IFacultyAdd,
+    IDepartment, IDepartmentAdd, ICourse, ICourseAdd, ICertificate,
+    ICertificateAdd, ICertificateCourse, ICertificateCourseAdd)
+from waeup.sirp.browser.resources import warning, datepicker, tabs, datatable
+from waeup.sirp.interfaces import(
+    IWAeUPObject, IUserContainer, IUserAccount, IDataCenter,
+    IWAeUPXMLImporter, IWAeUPXMLExporter, IBatchProcessor,
+    ILocalRolesAssignable, DuplicationError)
+from waeup.sirp.permissions import get_users_with_local_roles, getRoles
+from waeup.sirp.university.catalog import search
+from waeup.sirp.university.vocabularies import course_levels
+from waeup.sirp.users import LocalRoleSetEvent
+from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
+from waeup.sirp.authentication import get_principal_role_manager
+
+
+grok.context(IWAeUPObject)
+grok.templatedir('templates')
+
+def add_local_role(view, tab, **data):
+    form = view.request.form
+    localrole = form['local_role']
+    if form.has_key('user'):
+        user = form['user']
+    else:
+        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))
+    #import pdb; pdb.set_trace()
+    view.redirect(view.url(view.context, u'@@manage')+'#tab-%s' % tab)
+    return
+
+def del_local_roles(view, tab, **data):
+    form = view.request.form
+    if form.has_key('role_id'):
+        child_id = form['role_id']
+    else:
+        view.flash('No local role selected.')
+        view.redirect(view.url(view.context, '@@manage')+'#tab-%s' % tab)
+        return
+    if not isinstance(child_id, list):
+        child_id = [child_id]
+    deleted = []
+    role_manager = IPrincipalRoleManager(view.context)
+    for id in child_id:
+        localrole = id.split('|')[1]
+        user_name = id.split('|')[0]
+        try:
+            role_manager.unsetRoleForPrincipal(localrole, user_name)
+            notify(LocalRoleSetEvent(view.context, localrole, user_name, granted=False))
+            deleted.append(id)
+        except:
+            view.flash('Could not remove %s: %s: %s' % (
+                    id, sys.exc_info()[0], sys.exc_info()[1]))
+    if len(deleted):
+        view.flash('Successfully removed: %s' % ', '.join(deleted))
+    view.redirect(view.url(view.context, u'@@manage')+'#tab-%s' % tab)
+    return
+
+#
+# Login/logout pages...
+#
+
+class LoginPage(WAeUPPage):
+    """A login page, available for all objects.
+    """
+    grok.name('login')
+    grok.context(IWAeUPObject)
+    grok.require('waeup.Public')
+    title = u'Login'
+    label = title
+    camefrom = None
+
+    def isStudent(self):
+        prm = get_principal_role_manager()
+        roles = [x[0] for x in prm.getRolesForPrincipal(self.request.principal.id)]
+        return 'waeup.Student' in roles
+
+    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.isStudent():
+                    rel_link = '/students/%s' % self.request.principal.id
+                    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(WAeUPPage):
+    """A logout page. Calling this page will log the current user out.
+    """
+    grok.context(IWAeUPObject)
+    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())
+
+#
+# University related pages...
+#
+
+class UniversityPage(WAeUPDisplayFormPage):
+    """ The main university page.
+    """
+    grok.require('waeup.Public')
+    grok.name('index')
+    grok.context(IUniversity)
+    pnav = 0
+
+    form_fields = grok.AutoFields(IUniversity)
+    form_fields['frontpage'].custom_widget = ReSTDisplayWidget
+
+    @property
+    def title(self):
+        return "Welcome"
+
+    @property
+    def label(self):
+        return self.context.title
+
+class UniversityManageFormPage(WAeUPEditFormPage):
+    """Manage the basic properties of a `University` instance.
+    """
+    form_fields = grok.AutoFields(IUniversity)
+    grok.context(IUniversity)
+    grok.require('waeup.manageUniversity')
+    title = u'Edit portal settings'
+    pnav = 0
+    grok.name('manage')
+
+    @grok.action('Save')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        self.flash('Settings have been saved.')
+        return
+
+    @grok.action('Save and return')
+    def saveAndReturn(self, **data):
+        self.applyData(self.context, **data)
+        self.redirect(self.url(self.context))
+        self.flash('Settings have been saved.')
+        return
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.flash('Action cancelled.')
+        self.redirect(self.url(self.context))
+        return
+
+    @grok.action('Update plugins', validator=NullValidator)
+    def updatePlugins(self, **data):
+        self.context.updatePlugins()
+        self.flash('Plugins were updated. See log file for details.')
+        self.redirect(self.url(self.context))
+        return
+
+class AdministrationPage(WAeUPPage):
+    """ The administration overview page.
+    """
+    grok.name('administration')
+    grok.context(IUniversity)
+    grok.require('waeup.manageUniversity')
+    title = 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 ()
+
+#
+# User container pages...
+#
+
+class UserContainerPage(WAeUPPage):
+    """Overview page for all local users.
+    """
+    grok.require('waeup.manageUsers')
+    grok.context(IUserContainer)
+    grok.name('index')
+    title = 'Portal Users'
+
+    def update(self, userid=None, adduser=None, edit=None, delete=None):
+        if edit is not None and userid is not None:
+            self.redirect(self.url(userid))
+        if delete is not None and userid is not None:
+            self.context.delUser(userid)
+            self.flash('User %s successfully deleted.' % 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(getRoles())[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: %s <br />' %(role_title,
+                objects_string.rstrip(', '))
+        return local_roles_string
+
+    def getGlobalRoles(self,account):
+        global_roles = account.roles
+        global_roles_string = ''
+        for global_role in global_roles:
+            role_title = dict(getRoles())[global_role].title
+            global_roles_string += '%s <br />' % role_title
+        return global_roles_string
+
+class AddUserFormPage(WAeUPAddFormPage):
+    grok.require('waeup.manageUsers')
+    grok.context(IUserContainer)
+    grok.name('add')
+    form_fields = grok.AutoFields(IUserAccount)
+    label = ''
+    title = 'Add user'
+
+    @grok.action('Add user')
+    def addUser(self, **data):
+        name = data['name']
+        title = data['title']
+        description = data['description']
+        password = data['password']
+        roles = data['roles']
+        try:
+            self.context.addUser(name, password, title=title,
+                                 description=description, roles=roles)
+        except KeyError:
+            self.status = self.flash('The userid chosen already exists '
+                                  'in the database.')
+            return
+        self.redirect(self.url(self.context))
+
+class UserEditFormPage(WAeUPEditFormPage):
+    """Edit a user account.
+    """
+    grok.context(IUserAccount)
+    grok.name('index')
+
+    form_fields = grok.AutoFields(IUserAccount)
+    grok.require('waeup.manageUsers')
+
+    title = "Edit user"
+
+    @grok.action('Save')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        self.flash('User settings have been saved.')
+        return
+
+    @grok.action('Save and return')
+    def saveAndReturn(self, **data):
+        self.applyData(self.context, **data)
+        self.flash('User settings have been saved.')
+        self.redirect(self.url(self.context.__parent__))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context.__parent__))
+        return
+
+#
+# Search pages...
+#
+
+class SearchPage(WAeUPPage):
+    """General search page for the academics section.
+    """
+    grok.context(IFacultyContainer)
+    grok.name('search')
+    grok.template('searchpage')
+    grok.require('waeup.manageUniversity')
+    title = u"Academics"
+    label = u"Search Academic Section"
+    pnav = 1
+
+    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
+
+#
+# Contact forms...
+#
+class ContactAdminForm(WAeUPPage):
+    grok.context(IUniversity)
+    grok.name('contactadmin')
+    grok.template('contactadminform')
+    grok.require('waeup.Public')
+    pnav = 2
+
+    def title(self):
+        """Return True if the calling user is authenticated.
+        """
+        userid = self.request.principal.id
+        if userid != 'zope.anybody':
+            tt = u'Contact'
+        else:
+            tt = u'Enquiries'
+        return tt
+
+    def label(self):
+        return self.title()
+
+    @property
+    def namefield_label(self):
+        if self.request.principal.title == 'Applicant':
+            return 'Access Code'
+        else:
+            return 'Full Name'
+
+    def update(self, *args, **kw):
+        form = self.request.form
+        if not ('fullname' in form and 'email' in form and 'descr' in form):
+            return
+        self.fullname = fullname = form['fullname']
+        email = form['email']
+        self.descr = descr = form['descr']
+        regno =form['regno']
+        if not (fullname and email and descr):
+            self.flash('Error: All fields must be filled.')
+            return
+        if not re.match("^[a-zA-Z0-9._%-]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$",
+                        email):
+            self.flash('Error: %s is not a valid email address.' % email)
+            return
+        if regno == 'zope.anybody':
+            regno = 'Anonymous User'
+        text = """Fullname: %s
+Member ID: %s
+Description: %s
+"""
+        msg = MIMEText(text % (fullname,regno,descr))
+        msg['From'] = '%s <%s>' % (fullname,email)
+
+        # These parameters should be part of the portal configuration.
+        msg['To'] = 'contact@waeup.org'
+        msg['Subject'] = 'WAeUP Contact'
+        #server = smtplib.SMTP('smtp_server')
+        # Also connection details (username, passwords, host, port,
+        # etc.) should be settable somewhere.
+        # server.login('user_name','secret_password')
+        server = smtplib.SMTP('localhost')
+        server.sendmail(email,'contact@waeup.org',msg.as_string())
+
+        server.quit()
+        self.flash('Your message has been sent.')
+        return
+
+#
+# Datacenter pages...
+#
+
+class DatacenterPage(WAeUPPage):
+    grok.context(IDataCenter)
+    grok.name('index')
+    grok.require('waeup.manageUniversity')
+    title = u'Data Center'
+    pnav = 0
+
+class DatacenterUploadPage(WAeUPPage):
+    grok.context(IDataCenter)
+    grok.name('upload')
+    grok.require('waeup.manageUniversity')
+    title = u'Data Center Upload'
+    pnav = 0
+
+    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(WAeUPPage):
+    """Manual import step 1: choose file
+    """
+    grok.context(IDataCenter)
+    grok.name('import1')
+    grok.template('datacenterimport1page')
+    grok.require('waeup.manageUniversity')
+    title = u'Process CSV file'
+    pnav = 0
+
+    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(WAeUPPage):
+    """Manual import step 2: choose importer
+    """
+    grok.context(IDataCenter)
+    grok.name('import2')
+    grok.template('datacenterimport2page')
+    grok.require('waeup.manageUniversity')
+    title = u'Process CSV file'
+    pnav = 0
+
+    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
+        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 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_mode'] = self.mode
+        session['import_importer'] = self.importer
+        if proceed is not None:
+            self.redirect(self.url(self.context, '@@import3'))
+            return
+        self.fullpath = os.path.join(self.context.storage, self.filename)
+        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
+
+class DatacenterImportStep3(WAeUPPage):
+    """Manual import step 3: modify header
+    """
+    grok.context(IDataCenter)
+    grok.name('import3')
+    grok.template('datacenterimport3page')
+    grok.require('waeup.manageUniversity')
+    title = u'Process CSV file'
+    pnav = 0
+
+    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)
+        return result
+
+    @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'))
+
+class DatacenterImportStep4(WAeUPPage):
+    """Manual import step 4: do actual import
+    """
+    grok.context(IDataCenter)
+    grok.name('import4')
+    grok.template('datacenterimport4page')
+    grok.require('waeup.manageUniversity')
+    title = u'Process CSV file'
+    pnav = 0
+
+    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
+        if back is not None:
+            self.redirect(self.url(self.context, '@@import3'))
+            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 %d rows failed.' % self.warn_num)
+        self.flash('Successfully processed %s rows' % (
+                linenum - (self.warn_num)))
+
+class DatacenterLogsOverview(WAeUPPage):
+    grok.context(IDataCenter)
+    grok.name('logs')
+    grok.template('datacenterlogspage')
+    grok.require('waeup.manageUniversity')
+    title = u'Data Center Logs'
+    pnav = 0
+
+    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(WAeUPPage):
+    grok.context(IDataCenter)
+    grok.name('show')
+    grok.template('datacenterlogsshowfilepage')
+    grok.require('waeup.manageUniversity')
+    title = u'Show file'
+    pnav = 0
+
+    def update(self, show=None, remove=None, back=None):
+        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
+        self.files = self.context.getLogFiles()
+        fullpath = os.path.join(self.context.storage, 'logs', logname)
+        self.filecontents = open(fullpath, 'rb').read()
+
+class DatacenterSettings(WAeUPPage):
+    grok.context(IDataCenter)
+    grok.name('manage')
+    grok.template('datacentermanagepage')
+    grok.require('waeup.manageUniversity')
+    title = u'Data Center Settings'
+    pnav = 0
+
+    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): %s' % name)
+        except:
+            self.flash('Given storage path cannot be used.')
+            self.flash('Error: %s' %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 = IWAeUPXMLExporter(self.context)
+        xml = exporter.export().read()
+        self.response.setHeader(
+            'Content-Type', 'text/xml; charset=UTF-8')
+        return xml
+
+class ImportXMLPage(WAeUPPage):
+    """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 = IWAeUPXMLImporter(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
+
+
+
+#
+# FacultyContainer pages...
+#
+
+class FacultyContainerPage(WAeUPPage):
+    """ Index page for faculty containers.
+    """
+    grok.context(IFacultyContainer)
+    grok.require('waeup.View')
+    grok.name('index')
+    title = 'Academics'
+    label = 'Academic Section'
+    pnav = 1
+    grok.template('facultypage')
+
+class FacultyContainerManageFormPage(WAeUPEditFormPage):
+    """Manage the basic properties of a `Faculty` instance.
+    """
+    grok.context(IFacultyContainer)
+    grok.name('manage')
+    grok.require('waeup.manageUniversity')
+    grok.template('facultycontainermanagepage')
+    pnav = 1
+    taboneactions = ['Add faculty', 'Remove selected','Cancel']
+    subunits = 'Faculties'
+    title = 'Academics'
+
+    @property
+    def label(self):
+        return 'Manage academic section'
+
+    def update(self):
+        tabs.need()
+        #warning.need()
+        return super(FacultyContainerManageFormPage, self).update()
+
+    # ToDo: Show warning message before deletion
+    @grok.action('Remove selected')
+    def delFaculties(self, **data):
+        form = self.request.form
+        if form.has_key('val_id'):
+            child_id = form['val_id']
+        else:
+            self.flash('No faculty selected.')
+            self.redirect(self.url(self.context, '@@manage')+'#tab-1')
+            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: %s' % ', '.join(deleted))
+        self.redirect(self.url(self.context, '@@manage')+'#tab-1')
+        return
+
+    @grok.action('Add faculty', validator=NullValidator)
+    def addFaculty(self, **data):
+        self.redirect(self.url(self.context, '@@add'))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+
+class FacultyAddFormPage(WAeUPAddFormPage):
+    """ Page form to add a new faculty to a faculty container.
+    """
+    grok.context(IFacultyContainer)
+    grok.require('waeup.manageUniversity')
+    grok.name('add')
+    label = 'Add faculty'
+    title = 'Academics'
+    form_fields = grok.AutoFields(IFacultyAdd)
+    pnav = 1
+
+    @grok.action('Add faculty')
+    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')+'#tab-1')
+
+    @grok.action('Cancel')
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+#
+# Faculty pages
+#
+class FacultyPage(WAeUPPage):
+    """Index page of faculties.
+    """
+    grok.context(IFaculty)
+    grok.require('waeup.View')
+    grok.name('index')
+    pnav = 1
+
+    @property
+    def title(self):
+        return self.context.longtitle()
+
+    @property
+    def label(self):
+        return 'Departments'
+
+class FacultyManageFormPage(WAeUPEditFormPage):
+    """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'
+
+    @property
+    def title(self):
+        return self.context.longtitle()
+
+    def update(self):
+        tabs.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)
+
+    # ToDo: Show warning message before deletion
+    @grok.action('Remove selected')
+    def delSubobjects(self, **data):
+        form = self.request.form
+        if form.has_key('val_id'):
+            child_id = form['val_id']
+        else:
+            self.flash('No department selected.')
+            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
+            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: %s' % ', '.join(deleted))
+        self.redirect(self.url(self.context, '@@manage')+'#tab-2')
+        return
+
+    @grok.action('Save')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @grok.action('Add department', validator=NullValidator)
+    def addSubunit(self, **data):
+        self.redirect(self.url(self.context, '@@add'))
+        return
+
+    @grok.action('Add local role', validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self, '3', **data)
+
+    @grok.action('Remove selected local roles')
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,3,**data)
+
+class DepartmentAddFormPage(WAeUPAddFormPage):
+    """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
+
+    @property
+    def title(self):
+        return self.context.longtitle()
+
+    @grok.action('Add department')
+    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.redirect(self.url(self.context, u'@@manage')+'#tab-2')
+
+    @grok.action('Cancel')
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+
+#
+# Department pages
+#
+class DepartmentPage(WAeUPPage):
+    """Department index page.
+    """
+    grok.context(IDepartment)
+    grok.require('waeup.View')
+    grok.name('index')
+    pnav = 1
+    label = 'Courses and Certificates'
+
+    @property
+    def title(self):
+        return self.context.longtitle()
+
+    def update(self):
+        tabs.need()
+        datatable.need()
+        super(DepartmentPage, self).update()
+        return
+
+    def getCourses(self):
+        """Get a list of all stored courses.
+        """
+        for key, val in self.context.courses.items():
+            url = self.url(val)
+            yield(dict(url=url, name=key, container=val))
+
+    def getCertificates(self):
+        """Get a list of all stored certificates.
+        """
+        for key, val in self.context.certificates.items():
+            url = self.url(val)
+            yield(dict(url=url, name=key, container=val))
+
+class DepartmentManageFormPage(WAeUPEditFormPage):
+    """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'
+
+    @property
+    def title(self):
+        return self.context.longtitle()
+
+    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()
+        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)
+
+    @grok.action('Save')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        return
+
+    # ToDo: Show warning message before deletion
+    @grok.action('Remove selected courses')
+    def delCourses(self, **data):
+        form = self.request.form
+        if form.has_key('val_id'):
+            child_id = form['val_id']
+        else:
+            self.flash('No course selected.')
+            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        deleted = []
+        for id in child_id:
+            try:
+                del self.context.courses[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: %s' % ', '.join(deleted))
+        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
+        return
+
+    @grok.action('Remove selected certificates')
+    def delCertificates(self, **data):
+        form = self.request.form
+        if form.has_key('val_id'):
+            child_id = form['val_id']
+        else:
+            self.flash('No certificate selected.')
+            self.redirect(self.url(self.context, '@@manage')+'#tab-3')
+            return
+        if not isinstance(child_id, list):
+            child_id = [child_id]
+        deleted = []
+        for id in child_id:
+            try:
+                del self.context.certificates[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: %s' % ', '.join(deleted))
+        self.redirect(self.url(self.context, u'@@manage')+'#tab-3')
+        return
+
+    @grok.action('Add course', validator=NullValidator)
+    def addCourse(self, **data):
+        self.redirect(self.url(self.context, 'addcourse'))
+        return
+
+    @grok.action('Add certificate', validator=NullValidator)
+    def addCertificate(self, **data):
+        self.redirect(self.url(self.context, 'addcertificate'))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+    @grok.action('Add local role', validator=NullValidator)
+    def addLocalRole(self, **data):
+        return add_local_role(self, 4, **data)
+
+    @grok.action('Remove selected local roles')
+    def delLocalRoles(self, **data):
+        return del_local_roles(self,4,**data)
+
+class CourseAddFormPage(WAeUPAddFormPage):
+    """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
+
+    @property
+    def title(self):
+        return self.context.longtitle()
+
+    @grok.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 %s successfully created.' % (course.code)
+        self.flash(message)
+        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+class CertificateAddFormPage(WAeUPAddFormPage):
+    """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
+
+    @property
+    def title(self):
+        return self.context.longtitle()
+
+    @grok.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 %s successfully created.' % (certificate.code)
+        self.flash(message)
+        self.redirect(self.url(self.context, u'@@manage')+'#tab-3')
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self): #, **data):
+        self.redirect(self.url(self.context))
+        return
+
+#
+# Courses pages
+#
+class CoursePage(WAeUPPage):
+    """Course index page.
+    """
+    grok.context(ICourse)
+    grok.name('index')
+    grok.require('waeup.View')
+    pnav = 1
+    #form_fields = grok.AutoFields(ICourse)
+
+    @property
+    def title(self):
+        return 'Course: %s (%s)' % (self.context.title, self.context.code)
+
+class CourseManageFormPage(WAeUPEditFormPage):
+    """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)
+
+    @property
+    def title(self):
+        return 'Course: %s (%s)' % (self.context.title, self.context.code)
+
+    @grok.action('Save')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        return
+
+    @grok.action('Save and return')
+    def saveAndReturn(self, **data):
+        self.applyData(self.context, **data)
+        self.redirect(self.url(self.context))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+#
+# Certificate pages
+#
+class CertificatePage(WAeUPDisplayFormPage):
+    """Index page for certificates.
+    """
+    grok.context(ICertificate)
+    grok.name('index')
+    grok.require('waeup.View')
+    pnav = 1
+    form_fields = grok.AutoFields(ICertificate)
+    grok.template('certificatepage')
+
+    @property
+    def title(self):
+        return "Certificate: %s (%s)" % (self.context.title, self.context.code)
+
+    def update(self):
+        tabs.need()
+        #warning.need()
+        datatable.need()
+        return super(CertificatePage, self).update()
+
+class CertificateManageFormPage(WAeUPEditFormPage):
+    """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']
+
+    @property
+    def label(self):
+        return 'Manage certificate'
+
+    @property
+    def title(self):
+        return "Certificate: %s (%s)" % (self.context.title, self.context.code)
+
+    def update(self):
+        tabs.need()
+        #warning.need()
+        datatable.need()
+        return super(CertificateManageFormPage, self).update()
+
+    form_fields = grok.AutoFields(ICertificate)
+
+    @grok.action('Save')
+    def save(self, **data):
+        self.applyData(self.context, **data)
+        return
+
+    # ToDo: Show warning message before deletion
+    @grok.action('Remove selected course referrers')
+    def delCertificateCourses(self, **data):
+        form = self.request.form
+        if form.has_key('val_id'):
+            child_id = form['val_id']
+        else:
+            self.flash('No course referrer selected.')
+            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
+            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: %s' % ', '.join(deleted))
+        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
+        return
+
+
+    @grok.action('Add course referrer', validator=NullValidator)
+    def addCertificateCourse(self, **data):
+        self.redirect(self.url(self.context, 'addcertificatecourse'))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+
+class CertificateCourseAddFormPage(WAeUPAddFormPage):
+    """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'
+
+    @property
+    def title(self):
+        return "Certificate: %s (%s)" % (self.context.title, self.context.code)
+
+    @grok.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.redirect(self.url(self.context, u'@@manage')+'#tab-2')
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
+
+#
+# Certificate course pages...
+#
+class CertificateCoursePage(WAeUPPage):
+    """CertificateCourse index page.
+    """
+    grok.context(ICertificateCourse)
+    grok.name('index')
+    grok.require('waeup.View')
+    pnav = 1
+    #form_fields = grok.AutoFields(ICertificateCourse)
+
+    @property
+    def title(self):
+        return 'Course Referrer: %s' % (self.context.longtitle())
+
+    @property
+    def leveltitle(self):
+        return course_levels.getTerm(self.context.level).title
+
+class CertificateCourseManageFormPage(WAeUPEditFormPage):
+    """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
+
+    @property
+    def title(self):
+        return 'Course Referrer: %s' % (self.context.__name__)
+
+    @grok.action('Save and return')
+    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.redirect(self.url(parent))
+        return
+
+    @grok.action('Cancel', validator=NullValidator)
+    def cancel(self, **data):
+        self.redirect(self.url(self.context))
+        return
+
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/resources.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/resources.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/resources.py	(revision 6703)
@@ -0,0 +1,251 @@
+##
+## resources.py
+## Login : <uli@pu.smp.net>
+## Started on  Fri Mar 11 10:49:59 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
+##
+"""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::
+#:
+#:     <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 datepicker
+#:     ...
+#:     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:
+#:
+#:   <input type="submit" name="xyz" value="abc"/>
+#:          onclick="return confirmPost('Are you sure?')">
+warning = ResourceInclusion(waeup_sirp, 'warning.js')        
+
+#: A resource that binds jQueryUI tabs to <div> tags with class
+#: ``tabs``.
+#:
+#:
+#: If you want jQuery tabs for some <div>s 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 tabs
+#:     ...
+#:     class MyPage(...):
+#:       ...
+#:       def update(self):
+#:         tabs.need()
+#:
+#:   This way all required JavaScripts will be rendered correctly
+#:   into the HTML page generated.
+#:
+#: * In your HTML code add some ``<div>`` tag with ``class`` set to
+#    ``tabs``, like this::
+#:    
+#:      <div id="tabs">
+#:        <ul>
+#:          <li><a href="#tab-1"><span>Tab 1 Heading</span></a></li>
+#:          <li><a href="#tab-2"><span>Tab 2 Heading</span></a></li>
+#:        </ul>
+#:        <div id="tab-1">
+#:          Content of tab 1
+#:        </div>
+#:        <div id="tab-2">
+#:          Content of tab 2
+#:        </div>
+#:      </div>
+#:
+#: When the HTML is rendered, clicking into one of the tabs will make
+#: other <div>s disappear. See
+#:
+#:   http://jqueryui.com/demos/tabs/
+#:
+#: for details.
+#:
+tabs = ResourceInclusion(waeup_sirp, 'tabs.js',
+                         depends=[jquery, jqueryui])
+
+#: 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:
+#:    
+#:      <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 basic YUI based CSS as a resource.
+reset_fonts_grids = ResourceInclusion(
+    waeup_sirp, 'yuirfg.css')    
+    
+yuiapp_css = ResourceInclusion(
+    waeup_sirp, 'yuiapp.css',
+    depends=[reset_fonts_grids])     
+
+#: Register basic WAeUP base CSS (which is based on ``yuiapp.css`` as a
+#: resource.
+waeup_base_css = ResourceInclusion(
+    waeup_sirp, 'waeup-base.css',
+    depends=[yuiapp_css, base])
+
+#: The red WAeUP theme registered as a resource.
+waeuptheme_red1 = ResourceInclusion(
+    waeup_sirp, 'waeuptheme-red1.css',
+    depends=[waeup_base_css])
+
+#: The gray WAeUP theme registered as a resource.
+waeuptheme_gray1 = ResourceInclusion(
+    waeup_sirp, 'waeuptheme-gray1.css',
+    depends=[waeup_base_css])
+
+#: A basic theme based on jQuery only (crappy yet).
+waeuptheme_base = ResourceInclusion(
+    waeup_sirp, 'empty.css',
+    depends=[humanity])
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/datatable.js
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/datatable.js	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/datatable.js	(revision 6703)
@@ -0,0 +1,8 @@
+$(document).ready(function(){
+    $('.dataTable').dataTable();
+	$('.dataTableManage').dataTable( {
+		"aaSorting": [[ 1, "asc" ]],
+		"aoColumnDefs":[{ "bSortable": false, "aTargets": [ 0 ] }]
+	} );    
+});
+
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/datatables.css
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/datatables.css	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/datatables.css	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/browser/static/datepicker.js
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/datepicker.js	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/datepicker.js	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/browser/static/empty.css
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/empty.css	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/empty.css	(revision 6703)
@@ -0,0 +1,1 @@
+/* Nothing here */
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/jquery.dataTables.js
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/jquery.dataTables.js	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/jquery.dataTables.js	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/browser/static/jquery.dataTables.min.js
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/jquery.dataTables.min.js	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/jquery.dataTables.min.js	(revision 6703)
@@ -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];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/uli-studentpw/src/waeup/sirp/browser/static/mobile.css
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/mobile.css	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/mobile.css	(revision 6703)
@@ -0,0 +1,24 @@
+#primary-navigation li a {
+  font-size:100%;
+  padding-bottom: 0.5em;
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+  padding-top: 0.5em;
+}
+
+#user-navigation li a {
+  font-size: 86%;
+  padding-bottom: 0.5em;
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+  padding-top: 0.8em;
+}
+
+#hd {
+  padding-left:0.3em;
+  padding-right:0.3em;
+}
+
+#bd {
+  padding:0.6em;
+}
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/tabs.js
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/tabs.js	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/tabs.js	(revision 6703)
@@ -0,0 +1,4 @@
+/* See http://docs.jquery.com/UI/Tabs */
+$(document).ready(function() {
+    $("#tabs").tabs();
+  });
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/waeup-base.css
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/waeup-base.css	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/waeup-base.css	(revision 6703)
@@ -0,0 +1,478 @@
+/* This is the base stylesheet for WAeUP SIRP. It defines base styles
+additionally or modifying yuiapp.css styles and yuirfg.css. 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. */
+
+#hd { background-color:#5D0265; }
+.block .hd { background-color:#5D0265; color:#fff; border-bottom:10px solid #262626; }
+.block .bd h1 { font-size:153.9%; color:green; margin-bottom:0.5em;}
+.block .bd h2 { font-size:153.9%; color:#5D0265; margin-bottom:0.5em;}
+.block .bd h3 { font-size:130%; color:#5D0265; margin-bottom:0.5em;}
+.spaces .hd { background-color:transparent; }
+.spaces .hd ul li { background-color:#5D0265; }
+a { color:#5D0265; }
+.tabs .hd ul li a:hover { background-color:#8d0399; }
+
+/***** Henrik Stuff *****/
+
+.breadcrumbs { 
+  background-color: #fff; 
+  padding: 3px; 
+  margin-bottom: 1.3em 
+}
+
+a, table td a {
+  text-decoration: none;
+}
+
+a:hover {
+  text-decoration: underline;
+}
+
+.workflow {
+  border:1px solid #1fdf00;
+  background-color:#bbffb6;
+  font-family: courier;
+  font-size: 80%;
+  padding: 4px;
+  margin-bottom: 8px;
+}
+
+.form-status {
+    border:1px solid #fbb;
+    background-color:#fdd;
+    text-align:center;
+    padding: 2px;
+}
+
+.hint {
+    color: #CD0A0A;
+    padding-left: 4px;
+}
+
+
+.block {
+    margin-bottom: 0.5em;
+}
+
+.block .bd {
+    padding-bottom: 1em;
+}
+
+.actionButtons {
+    padding-top: 3em;
+}
+
+.wfstatus {
+  float: right;
+}
+
+.studentmenu {
+  text-align: center;
+  line-height: 2;
+}
+
+/* Define different styles for headlines in rendered restructured text */
+
+#rest h1 {
+  font-size:130%;
+}
+
+#rest h2 {
+  font-size:100%;
+}
+
+#rest h3 {
+  font-size:100%;
+}
+
+
+#rest h4 {
+  font-size:100%;
+}
+
+/* jquery-ui customizations */
+
+.ui-corner-all, .ui-corner-top {
+  -moz-border-radius-bottomleft: 0px;
+  -moz-border-radius-bottomright: 0px;
+  -moz-border-radius-topleft: 7px;
+  -moz-border-radius-topright: 7px;
+}
+
+.ui-widget {
+  font-family: Verdana,Arial,sans-serif;
+  font-size: 100%;
+}
+
+.ui-tabs .ui-tabs-nav li a:hover {
+  text-decoration: underline;
+}
+
+.dataTables_wrapper {
+  min-height: 100px;
+}
+
+.dataTables_info {
+  padding-top: 15px;
+}
+
+.dataTables_paginate {
+  padding-top: 15px;
+}
+
+
+/***** Uli Stuff *****/
+
+/* Make YUI button icons appear vertically centered... */
+.yui-skin-sam .yui-button a img {
+  vertical-align: middle;
+  margin-right: 3px;
+  margin-top: -1px;
+  margin-left: -2px;
+}
+
+/* Fix logo image height: as image is taller than h1 text, header area
+   explodes otherwise */
+#logo img {
+  margin-top: -45px;
+  position: relative;
+  top: 12px;
+}
+
+.actionbar { 
+  margin-top: -18px;
+  margin-bottom: 18px;
+}
+
+/* Form labels */
+form label, form label span {
+  color: #333;
+  font-weight: bold;
+  font-size: 100%;
+}
+
+/* Fixes to let tables appear correctly... */
+table, tr, th, td {font-family: arial,sans-serif;}
+
+/* Odd and even table rows */
+tr.even,
+.yui-skin-sam tr.yui-dt-even {
+  background-color: #fff;
+}
+tr.odd,
+.yui-skin-sam tr.yui-dt-odd {
+  background-color: #eee;
+}
+
+/* unselected paginator page numbers */
+.yui-skin-sam .yui-pg-pages a.yui-pg-page {
+  background-color: #fff;
+  color: #000;
+  text-decoration: none;
+  border: 1px solid #000;
+  padding-left: 5px;
+}
+
+/* selected paginator page, hover for unselected pages */
+.yui-skin-sam .yui-pg-pages a.yui-pg-page:hover,
+.yui-skin-sam span.yui-pg-page {
+  background-color: #000;
+  color: #fff;
+}
+
+/* paginator prev, next, etc. links */
+.yui-skin-sam .yui-dt .yui-dt-paginator a.yui-pg-first,
+.yui-skin-sam .yui-dt .yui-dt-paginator a.yui-pg-previous,
+.yui-skin-sam .yui-dt .yui-dt-paginator a.yui-pg-last,
+.yui-skin-sam .yui-dt .yui-dt-paginator a.yui-pg-next {
+  color: #000;
+  text-decoration: none;
+  background-color: #fff;
+}
+
+/* Header row */
+th .yui-dt-liner {
+  color: #fff;
+  background: #000 url();
+}
+
+/* Header cells text, when not selected */
+.yui-skin-sam .yui-dt th, .yui-skin-sam .yui-dt th a {
+  font-weight: bold;
+  color: #fff;
+}
+
+/* Sortable header cells, when selected */
+.yui-skin-sam th.yui-dt-desc .yui-dt-liner .yui-dt-sortable,
+.yui-skin-sam th.yui-dt-asc .yui-dt-liner .yui-dt-sortable {
+  color: #000;
+}
+
+/* Action buttons */
+
+.yui-button {
+	display: -moz-inline-box;
+	display: inline-block;
+	vertical-align: text-bottom;
+}
+.yui-button .first-child {
+	display: block;
+	*display: inline-block;
+}
+.yui-button button, .yui-button a {
+	display: block;
+	*display: inline-block;
+	border: none;
+	margin: 0;
+}
+.yui-button button {
+	background-color: transparent;
+	*overflow: visible;
+	cursor: pointer;
+}
+.yui-button a {
+	text-decoration: none;
+}
+.yui-skin-sam .yui-button {
+	border-width: 1px 0;
+	border-style: solid;
+	border-color: #808080;
+	background: url(sprite.png) repeat-x 0 0;
+	margin: auto .25em;
+}
+.yui-skin-sam .yui-button .first-child {
+	border-width: 0 1px;
+	border-style: solid;
+	border-color: #808080;
+	margin: 0 -1px;
+	_margin: 0;
+}
+.yui-skin-sam .yui-button button, .yui-skin-sam .yui-button a {
+	padding: 0 10px;
+	font-size: 93%;
+	line-height: 2;
+	*line-height: 1.7;
+	min-height: 2em;
+	*min-height: auto;
+	color: #000;
+}
+.yui-skin-sam .yui-button a {
+	*line-height: 1.875;
+	*padding-bottom: 1px;
+}
+
+
+/* The follwing is stolen from jquery... */
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled { cursor: default !important; }
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { display: block; 
+           text-indent: -99999px; overflow: hidden;
+           background-repeat: no-repeat; }
+
+/* states and images */
+.ui-icon { width: 16px; height: 16px; background-image: url(ui-icons_222222_256x240.png); }
+.ui-widget-content .ui-icon {background-image: url(ui-icons_222222_256x240.png); }
+.ui-widget-header .ui-icon {background-image: url(ui-icons_ffffff_256x240.png); }
+.ui-state-default .ui-icon { background-image: url(ui-icons_ef8c08_256x240.png); }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(ui-icons_ef8c08_256x240.png); }
+.ui-state-active .ui-icon {background-image: url(ui-icons_ef8c08_256x240.png); }
+.ui-state-highlight .ui-icon {background-image: url(ui-icons_228ef1_256x240.png); }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(ui-icons_e73434_256x240.png); }
+
+/* positioning */
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-off { background-position: -96px -144px; }
+.ui-icon-radio-on { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+span.ui-icon {
+               position: relative;
+               float: left;
+               margin-right: 4px;
+               margin-bottom: 0pt;
+               margin-left: -0pt;
+               left: 0.2em;
+               top: 50%;
+}
+a span.ui-icon { 
+                 margin-top: 4px;
+                 }
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/waeuptheme-gray1.css
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/waeuptheme-gray1.css	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/waeuptheme-gray1.css	(revision 6703)
@@ -0,0 +1,202 @@
+/* This is the bare minimum needed to create your own theme.
+In this example, all we're doing is overriding the default
+color scheme with our own. In the real world, you'd probably
+want to do more complex operations. */
+ 
+
+.breadcrumbs { 
+  padding: 0.3em; 
+  margin-bottom: 0.8em;
+} 
+
+#hd { background-color:#232C30;}
+.block .hd { background-color:#456; color:#fff; }
+.block .bd h1 { color:#456; }
+.block .bd h2 { color:#456; }
+.block .bd h3 { color:#456; }
+.spaces .hd { background-color:transparent; }
+.spaces .hd ul li { background-color:#456; }
+a { color:#456; }
+.tabs .hd ul li a:hover { background-color:#cc0000; }
+
+#bd { padding:0.8em; }
+
+.yui-t2 #yui-main .yui-b {
+  margin-left:12.9em;
+}
+
+#hd {padding:0.5em 0.4em 0;}
+
+.block .hd { 
+  padding:0.3em 1em; border-bottom:0px solid #262626;
+}
+
+table td, table th {
+  padding:0.3em;
+}
+
+table thead {
+  font-weight:normal;
+}
+
+#primary-navigation li a {
+  font-size:100%;
+  padding:0.3em 1em;
+}
+
+#user-navigation li a {
+  font-size:90%;
+  padding:0.4em 1em;
+}
+
+#ft {
+  padding:0em 1em;
+}
+
+#navigation {
+  background-color:#456;
+  margin-top:1.0em;
+}
+
+.rounded #navigation ul li, .rounded #navigation ul li a {
+  -moz-border-radius-topleft:4px;
+  -moz-border-radius-topright:4px;
+}
+
+.rounded #navigation, .rounded .hd, .rounded .hd ul li, .rounded .hd ul li a {
+  -moz-border-radius-topleft:0px;
+  -moz-border-radius-topright:0px;
+}
+
+.yui-skin-sam .yui-button button, .yui-skin-sam .yui-button a {
+  color:#000000;
+  font-size:93%;
+  line-height:1.6em;
+  min-height:1em;
+  padding:0 10px;
+}
+
+.yui-button a:hover {
+  text-decoration: none;
+}
+
+
+.actionbar { 
+  margin-top: -10px;
+  margin-bottom: 18px;
+  margin-left: -15px;
+}
+
+form label, form label span {
+  font-size: 100%;
+  color: #222222;
+  font-weight: normal;  
+}
+
+/* jquery-ui customizations */
+
+.ui-tabs {
+  padding-bottom: 0em;
+  padding-left: 0em;
+  padding-right: 0em;
+  padding-top: 0em;
+  position: relative;
+}
+
+.ui-corner-all {
+  -moz-border-radius:0px;
+}
+
+.ui-corner-top {
+  -moz-border-radius-topleft:4px;
+  -moz-border-radius-topright:4px;
+}
+
+.ui-tabs .ui-tabs-nav {
+  margin: 0;
+  padding-left: 0.2em;
+  padding-right: 0.2em;
+  padding-bottom: 0;
+  padding-top: 0;
+
+}
+
+.ui-tabs .ui-tabs-nav li {
+  float: center;
+  margin-left: 6px;
+  margin-right: 0.2em;
+  margin-top: 0;
+  padding-left: 0;
+  padding-right: 0;
+  padding-top: 0;
+  position: relative;
+  top: 1px;
+  white-space: nowrap;
+
+}
+
+.ui-tabs .ui-tabs-nav li a {
+  float: center;
+  padding-bottom: 0.5em;
+  padding-left: 1em;
+  padding-right: 1em;
+  padding-top: 0.5em;
+  text-decoration: none;
+}
+
+
+
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected {
+  margin-bottom: 0px;
+  padding-bottom: 0px;
+  background: #FFF;
+  border-width: 0px;
+  padding-top: 0px;
+  margin-top: 0px;
+}
+
+
+.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited {
+  color: #FFF;
+}
+
+.ui-state-active a:link, .ui-state-active a:visited {
+  color: #000;
+}
+
+.ui-widget-header, .ui-state-default, .ui-widget-header .ui-state-default {
+  
+  border-width: 0px;
+  background-color: #456;
+  background-image: none;
+  font-weight: normal;
+  color: white;
+}
+
+
+.ui-widget-header {
+  background-color: #456;
+  background-image: None;
+  border-color: #456;
+  border-style: solid;
+  border-width:0px;  
+}
+
+.ui-helper-reset {
+  font-size: 90%;
+  line-height: 1;
+}
+
+.ui-widget-content {
+  background-color: #FFFFFF;
+  background-image: none;
+  border-bottom-style: solid;
+  border-bottom-width: 1px;
+  border-top-style: solid;
+  border-top-width: 0px;
+  border-color: #456;
+}
+
+.ui-widget-content a {
+  color: #456;
+}
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/waeuptheme-red1.css
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/waeuptheme-red1.css	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/waeuptheme-red1.css	(revision 6703)
@@ -0,0 +1,24 @@
+/* This is the bare minimum needed to create your own theme.
+In this example, all we're doing is overriding the default
+color scheme with our own. In the real world, you'd probably
+want to do more complex operations. */
+ 
+#hd { background-color:#840000; }
+.block .hd { background-color:#840000; color:#fff; border-bottom:10px solid #262626; }
+.block .bd h1 { color:#840000; }
+.block .bd h2 { color:#840000; }
+.block .bd h3 { color:#840000; }
+.spaces .hd { background-color:transparent; }
+.spaces .hd ul li { background-color:#840000; }
+a { color:#840000; }
+.tabs .hd ul li a:hover { background-color:#cc0000; }
+
+.uni-logo {
+  background-image: url("your-logo.gif");
+  background-position: 0 13px;
+  background-repeat: no-repeat;
+  padding-bottom: 18px;
+  padding-left: 0;
+  padding-right: 135px;
+  padding-top: 40px;
+}
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/warning.js
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/warning.js	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/warning.js	(revision 6703)
@@ -0,0 +1,7 @@
+function confirmPost(question) {
+    var agree=confirm(question);
+    if (agree)
+    return true ;
+    else
+    return false ;
+};
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/yuiapp.css
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/yuiapp.css	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/yuiapp.css	(revision 6703)
@@ -0,0 +1,141 @@
+html, body { background-color:#f0f0ee; color:#222; }
+
+/* Make the page full bleed */
+#doc3 { margin:0; }
+
+/* Header */
+#hd { padding:1.2em 1.5em 0 1.5em; background-color:#7a1818; }
+h1 { font-size:230%; color:#fff; }
+
+#navigation { margin-top:1.5em; background-color:#262626; height:1%; }
+#navigation li { float:left; display:inline; }
+#navigation li a { display:block; color:#fff; text-decoration:none; }
+#navigation li a:hover { text-decoration:underline; }
+
+#primary-navigation li a { font-size:116%; padding:0.4em 1em; }
+#primary-navigation li.active a { background-color:#f0f0ee; color:#000; }
+
+#user-navigation { float:right; }
+#user-navigation li a { font-size:93%; padding:0.5em 1em; }
+
+/* Body */
+#bd { padding:1.5em; }
+h2 { font-size:153.9%; margin-bottom:0.5em; }
+h3 { font-size:153.9%; margin-bottom:0.5em; }
+
+/* Basic block */
+.block { margin-bottom:1.5em; }
+.block .hd { background-color:#7a1818; padding:0.7em 1em; color:#fff;
+             border-bottom:10px solid #262626; }
+.block .hd h2, .block .hd h3 { font-size:100%; margin-bottom:0; }
+.block .bd { padding:1em; background-color:#fff; }
+.block .bd h1 { color:#7a1818; }
+.block .bd h2 { color:#7a1818; }
+.block .bd h3 { color:#7a1818; }
+
+/* Extend block with tabs */
+.tabs .hd { padding:0; }
+.tabs .hd ul { height:1%; }
+.tabs .hd ul li { float:left; }
+.tabs .hd ul li a { display:block; color:#fff; text-decoration:none;
+                    padding:0.5em 1em; }
+.tabs .hd ul li a:hover { background-color:#470E0E; }
+.tabs .hd ul li.active a { background-color:#262626 !important; color:#fff;
+                           text-decoration:none; } /* important! fixes IE cascade issue */
+.tabs .hd h2, .tabs .hd h3 { position:absolute;
+                             margin-left:-5000px; } /* Hidden SEO Header if needed */
+
+/* Extend tab block with spaced tabs */
+.spaces .hd { background-color:transparent; }
+.spaces .hd ul li { background-color:#7a1818; margin-right:0.1em; }
+
+/* Extend block and header to have rounded corners */
+.rounded .hd,
+.rounded .hd ul li,
+.rounded .hd ul li a,
+.rounded #navigation,
+.rounded #navigation ul li,
+.rounded #navigation ul li a {
+    -moz-border-radius-topright:7px;
+    -moz-border-radius-topleft:7px;
+    -webkit-border-top-right-radius:7px;
+    -webkit-border-top-left-radius:7px;
+}
+
+/* Style the horizontal rules inside .block */
+.block hr { background-color:#f0f0ee; color:#f0f0ee; height:1px; border:0; }
+
+/* Tables */
+table { width:100%; margin-bottom:2em; }
+table td, table th { padding:0.5em; }
+table thead { background-color:#262626; color:#fff; font-weight:bold; }
+table thead a { text-decoration:none; color:#fff; }
+table thead a:hover { text-decoration:underline; }
+table tbody a { text-decoration:underline; }
+
+table.zebra tbody tr:nth-child(even) {background: #eee}
+table.zebra tbody tr:nth-child(odd) {background: #fff}
+
+
+
+/* Pager control*/
+ul.pager { text-align:right; }
+ul.pager li { display:inline; }
+ul.pager li a { padding:0.1em 0.3em; margin-right:0.3em; border:1px solid #000;
+                text-decoration:none; color:#000; }
+ul.pager li.active a, ul.pager li a:hover { background-color:#000; color:#fff; }
+
+
+
+/* Forms */
+form label { font-weight:bold; }
+form label span { font-weight:normal; color:#f00; font-size:85%; }
+form span.info { font-style:italic; color:#aaa; font-size:85%; }
+form .text { display:block; width:99%; border:1px solid #aaa; padding:0.3em; }
+form .column { width:48%; }
+form .left { float:left; }
+form .right { float:right; }
+
+/* Messages */
+.alert { padding:0.5em; text-align:center; }
+.error { border:2px solid #fbb; background-color:#fdd; }
+.warning { border:2px solid #fffaaa; background-color:#ffc; }
+.notice { border:2px solid #1fdf00; background-color:#bbffb6; }
+
+/* Lists */
+ol.list, ul.list, dl.list { margin-left:2em; margin-bottom:1em; }
+ol.list li { list-style:decimal outside; }
+ul.list li { list-style:disc outside; }
+dl.list dd { margin-left:1em; }
+
+ul.biglist { margin-bottom:1em; }
+ul.biglist li { margin-top:-1px; border:1px solid #f0f0ee;
+                border-width:1px 0 1px 0; }
+ul.biglist li a { display:block; padding:0.5em 0; text-decoration:none;
+                  color:#000; }
+ul.biglist li a:hover { text-decoration:underline; }
+
+/* Footer */
+#ft { text-align:center; font-size:85%; padding:0.5em 1em; color:#262626; }
+
+/* Generics */
+p { margin-bottom:1em; }
+a { color:#7a1818; }
+strong { font-weight:bold; }
+em { font-style:italic; }
+abbr, acronym { border-bottom:1px dotted #000; cursor:help; }
+
+.small { font-size:85%; }
+.gray { color:#999; }
+.highlight { background-color:#ffffcc; }
+.clear { clear:both; }
+.text-left { text-align:left; }
+.text-right { text-align:right; }
+.text-center { text-align:center; }
+
+/* datepicker */
+.ui-datepicker { background-color:#e0e0dd; }
+.ui-datepicker-title { font-weight: bold;  }
+
+/* use monospace font in textearea-based widgets */
+div.widget textarea { font-family: monospace; }
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/yuirfg.css
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/yuirfg.css	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/static/yuirfg.css	(revision 6703)
@@ -0,0 +1,317 @@
+/*Copyright (c) 2009, Yahoo! Inc. All rights reserved.Code licensed under the BSD License:http://developer.yahoo.net/yui/license.txtversion: 2.7.0*/
+html {
+	color: #000;
+	background: #FFF;
+}
+body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fieldset, legend, input, button, textarea, p, blockquote, th, td {
+	margin: 0;
+	padding: 0;
+}
+table {
+	border-collapse: collapse;
+	border-spacing: 0;
+}
+fieldset, img {
+	border: 0;
+}
+address, caption, cite, code, dfn, em, strong, th, var, optgroup {
+	font-style: inherit;
+	font-weight: inherit;
+}
+del, ins {
+	text-decoration: none;
+}
+li {
+	list-style: none;
+}
+caption, th {
+	text-align: left;
+}
+h1, h2, h3, h4, h5, h6 {
+	font-size: 100%;
+	font-weight: normal;
+}
+q:before, q:after {
+	content: '';
+}
+abbr, acronym {
+	border: 0;
+	font-variant: normal;
+}
+sup {
+	vertical-align: baseline;
+}
+sub {
+	vertical-align: baseline;
+}
+legend {
+	color: #000;
+}
+input, button, textarea, select, optgroup, option {
+	font-family: inherit;
+	font-size: inherit;
+	font-style: inherit;
+	font-weight: inherit;
+}
+input, button, textarea, select {
+	*font-size: 100%;
+}
+body {
+	font: 13px/1.231 arial, helvetica, clean, sans-serif;
+	*font-size: small;
+	*font: x-small;
+}
+select, input, button, textarea, button {
+	font: 99% arial, helvetica, clean, sans-serif;
+}
+table {
+	font-size: inherit;
+	font: 100%;
+}
+pre, code, kbd, samp, tt {
+	font-family: monospace;
+	*font-size: 108%;
+	line-height: 100%;
+}
+body {
+	text-align: center;
+}
+#doc, #doc2, #doc3, #doc4, .yui-t1, .yui-t2, .yui-t3, .yui-t4, .yui-t5, .yui-t6, .yui-t7 {
+	margin: auto;
+	text-align: left;
+	width: 57.69em;
+	*width: 56.25em;
+}
+#doc2 {
+	width: 73.076em;
+	*width: 71.25em;
+}
+#doc3 {
+	margin: auto 10px;
+	width: auto;
+}
+#doc4 {
+	width: 74.923em;
+	*width: 73.05em;
+}
+.yui-b {
+	position: relative;
+}
+.yui-b {
+	_position: static;
+}
+#yui-main .yui-b {
+	position: static;
+}
+#yui-main, .yui-g .yui-u .yui-g {
+	width: 100%;
+}
+.yui-t1 #yui-main, .yui-t2 #yui-main, .yui-t3 #yui-main {
+	float: right;
+	margin-left: -25em;
+}
+.yui-t4 #yui-main, .yui-t5 #yui-main, .yui-t6 #yui-main {
+	float: left;
+	margin-right: -25em;
+}
+.yui-t1 .yui-b {
+	float: left;
+	width: 12.30769em;
+	*width: 12.00em;
+}
+.yui-t1 #yui-main .yui-b {
+	margin-left: 13.30769em;
+	*margin-left: 13.05em;
+}
+.yui-t2 .yui-b {
+	float: left;
+	width: 13.8461em;
+	*width: 13.50em;
+}
+.yui-t2 #yui-main .yui-b {
+	margin-left: 14.8461em;
+	*margin-left: 14.55em;
+}
+.yui-t3 .yui-b {
+	float: left;
+	width: 23.0769em;
+	*width: 22.50em;
+}
+.yui-t3 #yui-main .yui-b {
+	margin-left: 24.0769em;
+	*margin-left: 23.62em;
+}
+.yui-t4 .yui-b {
+	float: right;
+	width: 13.8456em;
+	*width: 13.50em;
+}
+.yui-t4 #yui-main .yui-b {
+	margin-right: 14.8456em;
+	*margin-right: 14.55em;
+}
+.yui-t5 .yui-b {
+	float: right;
+	width: 18.4615em;
+	*width: 18.00em;
+}
+.yui-t5 #yui-main .yui-b {
+	margin-right: 19.4615em;
+	*margin-right: 19.125em;
+}
+.yui-t6 .yui-b {
+	float: right;
+	width: 23.0769em;
+	*width: 22.50em;
+}
+.yui-t6 #yui-main .yui-b {
+	margin-right: 24.0769em;
+	*margin-right: 23.62em;
+}
+.yui-t7 #yui-main .yui-b {
+	display: block;
+	margin: 0 0 1em 0;
+}
+#yui-main .yui-b {
+	float: none;
+	width: auto;
+}
+.yui-gb .yui-u, .yui-g .yui-gb .yui-u, .yui-gb .yui-g, .yui-gb .yui-gb, .yui-gb .yui-gc, .yui-gb .yui-gd, .yui-gb .yui-ge, .yui-gb .yui-gf, .yui-gc .yui-u, .yui-gc .yui-g, .yui-gd .yui-u {
+	float: left;
+}
+.yui-g .yui-u, .yui-g .yui-g, .yui-g .yui-gb, .yui-g .yui-gc, .yui-g .yui-gd, .yui-g .yui-ge, .yui-g .yui-gf, .yui-gc .yui-u, .yui-gd .yui-g, .yui-g .yui-gc .yui-u, .yui-ge .yui-u, .yui-ge .yui-g, .yui-gf .yui-g, .yui-gf .yui-u {
+	float: right;
+}
+.yui-g div.first, .yui-gb div.first, .yui-gc div.first, .yui-gd div.first, .yui-ge div.first, .yui-gf div.first, .yui-g .yui-gc div.first, .yui-g .yui-ge div.first, .yui-gc div.first div.first {
+	float: left;
+}
+.yui-g .yui-u, .yui-g .yui-g, .yui-g .yui-gb, .yui-g .yui-gc, .yui-g .yui-gd, .yui-g .yui-ge, .yui-g .yui-gf {
+	width: 49.1%;
+}
+.yui-gb .yui-u, .yui-g .yui-gb .yui-u, .yui-gb .yui-g, .yui-gb .yui-gb, .yui-gb .yui-gc, .yui-gb .yui-gd, .yui-gb .yui-ge, .yui-gb .yui-gf, .yui-gc .yui-u, .yui-gc .yui-g, .yui-gd .yui-u {
+	width: 32%;
+	margin-left: 1.99%;
+}
+.yui-gb .yui-u {
+	*margin-left: 1.9%;
+	*width: 31.9%;
+}
+.yui-gc div.first, .yui-gd .yui-u {
+	width: 66%;
+}
+.yui-gd div.first {
+	width: 32%;
+}
+.yui-ge div.first, .yui-gf .yui-u {
+	width: 74.2%;
+}
+.yui-ge .yui-u, .yui-gf div.first {
+	width: 24%;
+}
+.yui-g .yui-gb div.first, .yui-gb div.first, .yui-gc div.first, .yui-gd div.first {
+	margin-left: 0;
+}
+.yui-g .yui-g .yui-u, .yui-gb .yui-g .yui-u, .yui-gc .yui-g .yui-u, .yui-gd .yui-g .yui-u, .yui-ge .yui-g .yui-u, .yui-gf .yui-g .yui-u {
+	width: 49%;
+	*width: 48.1%;
+	*margin-left: 0;
+}
+.yui-g .yui-g .yui-u {
+	width: 48.1%;
+}
+.yui-g .yui-gb div.first, .yui-gb .yui-gb div.first {
+	*margin-right: 0;
+	*width: 32%;
+	_width: 31.7%;
+}
+.yui-g .yui-gc div.first, .yui-gd .yui-g {
+	width: 66%;
+}
+.yui-gb .yui-g div.first {
+	*margin-right: 4%;
+	_margin-right: 1.3%;
+}
+.yui-gb .yui-gc div.first, .yui-gb .yui-gd div.first {
+	*margin-right: 0;
+}
+.yui-gb .yui-gb .yui-u, .yui-gb .yui-gc .yui-u {
+	*margin-left: 1.8%;
+	_margin-left: 4%;
+}
+.yui-g .yui-gb .yui-u {
+	_margin-left: 1.0%;
+}
+.yui-gb .yui-gd .yui-u {
+	*width: 66%;
+	_width: 61.2%;
+}
+.yui-gb .yui-gd div.first {
+	*width: 31%;
+	_width: 29.5%;
+}
+.yui-g .yui-gc .yui-u, .yui-gb .yui-gc .yui-u {
+	width: 32%;
+	_float: right;
+	margin-right: 0;
+	_margin-left: 0;
+}
+.yui-gb .yui-gc div.first {
+	width: 66%;
+	*float: left;
+	*margin-left: 0;
+}
+.yui-gb .yui-ge .yui-u, .yui-gb .yui-gf .yui-u {
+	margin: 0;
+}
+.yui-gb .yui-gb .yui-u {
+	_margin-left: .7%;
+}
+.yui-gb .yui-g div.first, .yui-gb .yui-gb div.first {
+	*margin-left: 0;
+}
+.yui-gc .yui-g .yui-u, .yui-gd .yui-g .yui-u {
+	*width: 48.1%;
+	*margin-left: 0;
+}
+.yui-gb .yui-gd div.first {
+	width: 32%;
+}
+.yui-g .yui-gd div.first {
+	_width: 29.9%;
+}
+.yui-ge .yui-g {
+	width: 24%;
+}
+.yui-gf .yui-g {
+	width: 74.2%;
+}
+.yui-gb .yui-ge div.yui-u, .yui-gb .yui-gf div.yui-u {
+	float: right;
+}
+.yui-gb .yui-ge div.first, .yui-gb .yui-gf div.first {
+	float: left;
+}
+.yui-gb .yui-ge .yui-u, .yui-gb .yui-gf div.first {
+	*width: 24%;
+	_width: 20%;
+}
+.yui-gb .yui-ge div.first, .yui-gb .yui-gf .yui-u {
+	*width: 73.5%;
+	_width: 65.5%;
+}
+.yui-ge div.first .yui-gd .yui-u {
+	width: 65%;
+}
+.yui-ge div.first .yui-gd div.first {
+	width: 32%;
+}
+#hd:after, #bd:after, #ft:after, .yui-g:after, .yui-gb:after, .yui-gc:after, .yui-gd:after, .yui-ge:after, .yui-gf:after {
+	content: ".";
+	display: block;
+	height: 0;
+	clear: both;
+	visibility: hidden;
+}
+#hd, #bd, #ft, .yui-g, .yui-gb, .yui-gc, .yui-gd, .yui-ge, .yui-gf {
+	zoom: 1;
+}
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/actionbutton.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/actionbutton.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/actionbutton.pt	(revision 6703)
@@ -0,0 +1,10 @@
+<span class="yui-button">
+  <span class="first-child">
+    <a href=""
+       tal:attributes="href viewlet/target_url">
+      <img src="" alt="edit"
+	   tal:attributes="src viewlet/icon_url;
+			   alt viewlet/alt"/>
+    <span tal:content="viewlet/text">TEXT</span></a>
+  </span>
+</span>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/addaction.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/addaction.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/addaction.pt	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/browser/templates/administrationpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/administrationpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/administrationpage.pt	(revision 6703)
@@ -0,0 +1,7 @@
+<h3>Administration</h3>
+
+<p>
+  You can manage the following tasks:
+</p>
+
+<tal:admintasks tal:content="structure provider:admintasks" />
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/admintask.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/admintask.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/admintask.pt	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/browser/templates/breadcrumbs.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/breadcrumbs.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/breadcrumbs.pt	(revision 6703)
@@ -0,0 +1,13 @@
+<div class="breadcrumbs">
+  <span tal:repeat="item viewlet/getEntries">
+    <a href="#"
+       tal:attributes="href item/url">
+      <span tal:content="item/title">
+	Entry
+      </span>
+    </a>
+    <span tal:condition="not: repeat/item/end">
+      >
+    </span>
+  </span>
+</div>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/certificatecoursepage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/certificatecoursepage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/certificatecoursepage.pt	(revision 6703)
@@ -0,0 +1,39 @@
+<h2 tal:content="context/longtitle">TITLE</h2>
+
+<table class="zebra">
+  <thead>
+  </thead>
+  <tbody>
+    <tr>
+      <td>Code:</td>
+      <td tal:content="context/__name__">CODE</td>
+    </tr>  
+    <tr>
+      <td>Course Code:</td>
+      <td tal:content="context/getCourseCode">THE COURSE</td>
+    </tr>    
+    <tr>
+      <td>Course Title:</td>
+      <td tal:content="context/course/title">THE TITLE</td>
+    </tr>
+    <tr>
+      <td>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>Level:</td>
+      <td tal:content="view/leveltitle">LEVEL</td>
+    </tr>
+    <tr>
+      <td>Is mandatory course (not elective):</td>
+      <td tal:content="context/core_or_elective">MANDATORY</td>
+    </tr>
+  </tbody>
+</table>
+
+
+
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/certificatemanagepage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/certificatemanagepage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/certificatemanagepage.pt	(revision 6703)
@@ -0,0 +1,119 @@
+<h2 i18n:translate=""
+tal:condition="view/label"
+tal:content="view/label">Label</h2>
+
+<div class="form-status"
+    tal:define="status view/status"
+    tal:condition="status">
+    Form Status:
+    <span i18n:translate="" tal:content="view/status">
+      Form status summary
+    </span>
+</div>
+
+<form action="." tal:attributes="action request/URL" method="POST"
+      class="edit-form" enctype="multipart/form-data"> 
+
+<div id="tabs">
+<ul>
+  <li><a href="#tab-1"><span>Settings</span></a></li>
+  <li><a href="#tab-2"><span>Course Referrers</span></a></li>
+</ul>
+   
+<div id="tab-1">        
+      <table class="zebra">
+      <tbody>
+      <tal:block repeat="widget view/widgets">
+        <tr>
+          <td class="label">
+            <label tal:attributes="for widget/name">
+              <span class="required" tal:condition="widget/required">*</span>
+              <span i18n:translate="" tal:content="widget/label">label</span>:
+            </label>
+          </td>
+          <td class="field">
+            <span class="widget" 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">
+              <span class="hint" tal:content="structure widget/hint">hint</span>
+            </tal:hint>
+          </td>
+        </tr>
+      </tal:block>
+      </tbody>
+      </table>
+      <div class="actionButtons" 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">
+  <h3>Course Referrers</h3>
+  <table class="display dataTableManage">
+    <thead>
+      <tr>
+        <th>&nbsp;</th><th>Level</th><th>Semester</th><th>Referrer</th><th>Course</th><th>Title</th>
+        <th>Mandatory</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr tal:repeat="cc context/values">
+         <td>
+          <input type="checkbox"
+    		 name="val_id"
+    		 tal:attributes="value cc/__name__" />
+        </td>
+         <td>
+          <span tal:content="cc/level">
+    	   LEVEL
+          </span>
+        </td>
+        <td tal:content="cc/course/semester">SEMESTER</td>
+        <td>
+          <a tal:attributes="href python: view.url(cc)"
+    	     tal:content="cc/__name__">
+        	REFERRER CODE
+          </a>
+        </td>
+        <td>
+          <a tal:attributes="href python:view.url(cc.course)"
+    	     tal:content="cc/course/code">
+        	COURSE CODE
+          </a>
+        </td>        
+        <td>
+          <span tal:content="cc/course/title">
+    	   COURSE TITLE
+          </span>
+        </td>
+        <td>
+          <span tal:content="cc/core_or_elective">
+    	   MANDATORY
+          </span>
+        </td>                
+      </tr>
+    </tbody>
+  </table>
+  <div class="actionButtons" 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/uli-studentpw/src/waeup/sirp/browser/templates/certificatepage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/certificatepage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/certificatepage.pt	(revision 6703)
@@ -0,0 +1,66 @@
+<h2 tal:content="context/longtitle">TITLE</h2>
+
+<table class="zebra">
+<thead>
+</thead>  
+<tbody>
+  <tal:block repeat="widget view/widgets">
+    <tr>
+      <td>
+        <label tal:attributes="for widget/name">
+          <span i18n:translate=""
+                         tal:content="widget/label">label</span>:
+        </label>
+      </td>
+      <td class="field" tal:on-error="default">
+        <div class="widget" tal:content="structure widget">
+          <input type="text" />
+        </div>
+      </td>
+    </tr>
+  </tal:block>
+</tbody>
+</table>
+
+<h3><span tal:content="context/__name__">Code</span> Course Referrers</h3>
+<br />
+<table class="display dataTable">
+<thead>
+  <tr>
+    <th>Level</th><th>Semester</th><th>Referrer</th><th>Course</th>
+    <th>Title</th><th>Mandatory</th>
+  </tr>
+</thead>
+<tbody>
+  <tr tal:repeat="cc context/values">
+     <td>
+      <span tal:content="cc/level">
+	   LEVEL
+      </span>
+    </td>
+    <td tal:content="cc/course/semester">SEMESTER</td>
+    <td>
+      <a tal:attributes="href python: view.url(cc)"
+	     tal:content="cc/__name__">
+    	REFERRER CODE
+      </a>
+    </td>
+    <td>
+      <a tal:attributes="href python:view.url(cc.course)"
+	     tal:content="cc/course/code">
+    	COURSE CODE
+      </a>     
+    <td>
+      <span tal:content="cc/course/title">
+	   COURSE TITLE
+      </span>
+    </td>
+    <td>
+      <span tal:content="cc/core_or_elective">
+	   MANDATORY
+      </span>
+    </td>                
+  </tr>
+</tbody>
+</table>
+
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/contactadminform.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/contactadminform.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/contactadminform.pt	(revision 6703)
@@ -0,0 +1,42 @@
+<h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+<form id="form1" name="form1" method="post">
+<input type="hidden" name="regno" type="text" id="regno"
+     tal:attributes="value layout/getUserId"/>
+<table width="100%" border="0" cellspacing="5" cellpadding="0">
+<tr>
+  <td width="150px">
+     <strong><span tal:replace="view/namefield_label" />: </strong>
+  </td>
+  <td tal:condition="not:layout/isAuthenticated">
+     <input name="fullname" type="text" id="fullname"/>
+  </td>
+  <td tal:condition="layout/isAuthenticated"> 
+    <span tal:content="layout/getUserTitle" />
+    <input type="hidden" name="fullname" type="text" id="fullname"
+       tal:attributes="value layout/getUserTitle"/>
+  </td>  
+</tr>
+<tr>
+  <td>
+     <strong>Email: </strong>
+  </td>
+  <td>
+     <input name="email" type="text" id="email" />
+  </td>
+</tr>
+<tr>
+  <td valign="top">
+     <strong>Text: </strong>
+  </td>
+  <td>
+     <textarea name="descr" rows="10" cols="60" id="descr"></textarea>
+  </td>
+</tr>
+<tr>
+  <td>&nbsp;</td>
+  <td><input name="Submit" type="submit" class="standalone" value="Submit" /></td>
+</tr>
+</table>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/coursepage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/coursepage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/coursepage.pt	(revision 6703)
@@ -0,0 +1,31 @@
+<h2 tal:content="context/title">TITLE</h2>
+
+<table class="listing">
+  <thead>
+  </thead>
+  <tbody>
+    <tr class="odd">
+      <td>Code:</td>
+      <td tal:content="context/__name__">CODE</td>
+    </tr>  
+    <tr class="even">
+      <td>Title:</td>
+      <td tal:content="context/title">THE TITLE</td>
+    </tr>
+    <tr class="odd">
+      <td>Credits:</td>
+      <td tal:content="context/credits">CREDITS</td>
+    </tr>
+    <tr class="even">
+      <td>Passmark:</td>
+      <td tal:content="context/passmark">PASSMARK</td>
+    </tr>
+    <tr class="odd">
+      <td>Semester/Term:</td>
+      <td tal:content="context/semester">SEMESTER</td>
+    </tr>
+  </tbody>
+</table>
+
+
+
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterimport1page.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterimport1page.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterimport1page.pt	(revision 6703)
@@ -0,0 +1,31 @@
+<h2>Process CSV file</h2>
+<h3>Step 1</h3>
+<p>
+  Using batch processing you can mass-create, mass-update, or
+  mass-remove datasets from the database using CSV files.
+</p>
+<p>
+  Please select a file for processing from the list below
+</p>
+<form method="POST">
+  <table>
+    <thead>
+      <th /><th>Filename</th><th>Datasets</th><th>Size</th><th>Date</th>
+    </thead>
+    <tbody>
+      <tr tal:repeat="file view/getFiles"
+	  tal:attributes="class python: repeat['file'].odd() and 'odd' or ''">
+	<td>
+	  <button type="submit" name="select"
+		  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" value="Cancel" />
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterimport2page.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterimport2page.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterimport2page.pt	(revision 6703)
@@ -0,0 +1,80 @@
+<h2>Process CSV file</h2>
+<h3>Step 2</h3>
+
+<form method="POST">
+  <p>
+    <b>File:</b> <span tal:content="view/filename">Filename.csv</span>
+    <input type="hidden" name="filename"
+	   tal:attributes="value view/filename" />
+  </p>
+  <p>
+    Please select a file-processor and a processing-mode from the 
+    selections below:
+  </p>
+
+  <div>
+    <table style="border: 1px solid #000;"
+	   tal:define="body view/getPreviewBody;
+		       headers view/getPreviewHeader;
+		       fieldnum python: len(headers)">
+      <thead>
+	<tr style="border: 1px solid #ccc;">
+	  <th style="border: 1px solid #888;"
+	      tal:repeat="headerfield headers"
+	      tal:content="headerfield">
+	    headerfield
+	  </th>
+	</tr>
+      </thead>
+      <tbody>
+	<tr tal:repeat="row body"
+	    tal:attributes="class python: repeat['row'].odd() and 'odd' or 'even'"
+	    style="border: 1px solid #000;">
+	  <td tal:repeat="value row"
+	      tal:content="value"
+	      style="border: 1px solid #000;">
+	    value
+	  </td>
+	</tr>
+      </tbody>
+    </table>
+  </div>
+
+  <div>
+    <b>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>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 type="submit" name="cancel" value="Cancel" />
+  <input type="submit" name="back1" value="Back to step 1" />
+  <input type="submit" name="proceed" value="Proceed to step 3..." />
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterimport3page.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterimport3page.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterimport3page.pt	(revision 6703)
@@ -0,0 +1,104 @@
+<h2>Process CSV file</h2>
+<h3>Step 3</h3>
+<div tal:define="warnings view/getWarnings">
+<form>
+
+  <p>
+    Eventually modify headerfields of import file below.
+  </p>
+
+  <p>
+    <b>File:</b> <span tal:content="view/filename">Filename.csv</span>
+    <input type="hidden" name="filename"
+	   tal:attributes="value view/filename" />
+  </p>
+  <p>
+    <b>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>Processing mode: </b>
+    <span tal:content="view/mode">mode</span>
+    <input type="hidden" name="mode"
+	   tal:attributes="value view/mode" />
+  </p>
+
+  <div>
+    <div class="alert error ui-state-error"
+	 tal:condition="warnings">
+      <span class="ui-icon ui-icon-alert">A</span>
+      Current settings will give the follwing warnings, when importing:
+      <div tal:repeat="warning warnings" 
+	   tal:content="warning">
+	Warning messages
+      </div>
+    </div>
+
+    <div class="alert notice ui-state-info"
+	 tal:condition="not: warnings">
+      <span class="ui-icon ui-icon-info">A</span>
+      Header fields OK
+    </div>
+  </div>
+
+
+  <div>
+    <table style="border: 1px solid #000;"
+	   tal:define="body view/getPreviewBody;
+		       headers view/headerfields;
+		       fieldnum python: len(headers)">
+      <thead>
+	<tr style="border: 1px solid #ccc;">
+	  <th style="border: 1px solid #888;"
+	      tal:repeat="headerfield headers"
+	      tal:content="headerfield">
+	    headerfield
+	  </th>
+	</tr>
+	<tr style="border: 1px solid #ccc;">
+	  <th style="border: 1px solid #888; text-align=center;"
+	      tal:repeat="headerfield headers">
+	    <i>change to:</i>
+	  </th>
+	</tr>
+	<tr style="border: 1px solid #ccc;">
+	  <th style="border: 1px solid #888;"
+	      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"
+	    tal:attributes="class python: repeat['row'].odd() and 'odd' or 'even'"
+	    style="border: 1px solid #000;">
+	  <td tal:repeat="value row"
+	      tal:content="value"
+	      style="border: 1px solid #000;">
+	    value
+	  </td>
+	</tr>
+      </tbody>
+    </table>
+  </div>
+
+
+  <input type="submit" name="cancel" value="Cancel" />
+  <input type="reset" name="reset" value="Reset" />
+  <input type="submit" name="update" value="Set headerfields" />
+  <br />
+  <input type="submit" name="back2" value="Back to step 2" />
+  <input tal:attributes="disabled python: len(warnings) and 'disabled' or ''"
+	 type="submit" name="proceed" value="Perform import..." />
+
+</form>
+</div>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterimport4page.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterimport4page.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterimport4page.pt	(revision 6703)
@@ -0,0 +1,22 @@
+<h2>Process CSV file</h2>
+<h3>Step 4</h3>
+<p>
+  Batch processing finished.
+</p>
+<p>
+  <b>File:</b> <span tal:content="view/filename">Filename.csv</span>
+</p>
+<p>
+  <b>Processor:</b>
+  <span tal:content="view/importer/name">Importer Name</span>
+</p>
+<p>
+  <b>Processing mode: </b>
+  <span tal:content="view/mode">mode</span>
+</p>
+<form method="POST">
+  <input tal:condition="view/warn_num"
+	 type="submit" name="back" value="Back to step 3" />
+  <input type="submit" name="finish" value="Back to data center" />
+  <input type="submit" name="showlog" value="View processing log" />
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterlogspage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterlogspage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterlogspage.pt	(revision 6703)
@@ -0,0 +1,39 @@
+<p>
+  Here you can see the local logfiles.
+</p>
+<p>
+  Use the buttons on the right to examine the files.
+</p>
+
+<div tal:define="files view/files">
+  <p tal:condition="not: files">
+    Currently no log files are available.
+  </p>
+  <table tal:condition="files">
+    <thead>
+      <tr>
+	<th></th><th>File</th><th>Date</th><th>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 type="submit" name="show" value="Show" />
+	    <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 type="submit" name="back" value="Back to Data Center" />
+  </form>
+
+</div>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterlogsshowfilepage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterlogsshowfilepage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterlogsshowfilepage.pt	(revision 6703)
@@ -0,0 +1,17 @@
+<h2>
+  Logfile <span tal:replace="view/filename">file.csv</span>
+</h2>
+
+<form method="POST">
+  <input type="submit" name="back" value="Back" />
+</form>
+
+<div class="bd">
+  <pre tal:content="view/filecontents">
+    blah, blah
+  </pre>
+</div>
+
+<form method="POST">
+  <input type="submit" name="back" value="Back" />
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacentermanagepage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacentermanagepage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacentermanagepage.pt	(revision 6703)
@@ -0,0 +1,20 @@
+<form method="POST">
+  <div>
+    <label for="newpath">Storage path:</label>
+    <input type="text" name="newpath"
+	   tal:attributes="value context/storage" />
+    <br /><br />
+    <div>
+      <input type="checkbox" name="move" checked="checked" />
+      <label for="move">Copy existing data to new storage?</label>
+    </div>
+    <div>
+      <input type="checkbox" name="overwrite" checked="no" />
+      <label for="overwrite">Overwrite contents in new location?</label>
+    </div>
+    <br />
+    <input type="submit" name="save" value="Save" />
+    <input type="reset" name="reset" value="Reset" />
+    <input type="submit" name="cancel" value="Cancel" />
+  </div>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenterpage.pt	(revision 6703)
@@ -0,0 +1,28 @@
+<h2>Data Center</h2>
+
+<p>
+The data center helps you to manage portal data. You can upload CSV
+files here, which will be available for import afterwards.
+</p>
+
+<p>
+  <b>Storage path:</b> <span tal:content="context/storage">/foo/bar</span>
+</p>
+
+<table>
+  <thead>
+    <tr>
+      <th>File</th>
+      <th>Size</th>
+      <th>Uploaded</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="file context/getFiles"
+	tal:attributes="class python: repeat['file'].odd() and 'odd' or 'even'">
+      <td tal:content="file/name">thefilename</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/uli-studentpw/src/waeup/sirp/browser/templates/datacenteruploadpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenteruploadpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/datacenteruploadpage.pt	(revision 6703)
@@ -0,0 +1,7 @@
+<form method="post" enctype="multipart/form-data">
+  <input type="file" name="uploadfile:file" />
+  <div>
+    <input type="submit" name="SUBMIT" value="Upload" />
+    <input type="submit" name="CANCEL" value="Cancel" />
+  </div>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/default_waeup_display_form.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/default_waeup_display_form.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/default_waeup_display_form.pt	(revision 6703)
@@ -0,0 +1,26 @@
+<h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+<table class="zebra">
+  <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 class="controls">
+      <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/uli-studentpw/src/waeup/sirp/browser/templates/default_waeup_edit_form.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/default_waeup_edit_form.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/default_waeup_edit_form.pt	(revision 6703)
@@ -0,0 +1,47 @@
+<form action="." tal:attributes="action request/URL" method="post"
+      class="edit-form" enctype="multipart/form-data">
+
+  <h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+  <div class="form-status"
+    tal:define="status view/status"
+    tal:condition="status">
+    Form Status:
+    <span i18n:translate="" tal:content="view/status">
+      Form status summary
+    </span>
+  </div>
+
+  <table class="zebra">
+    <tbody>
+      <tal:block repeat="widget view/widgets">
+        <tr>
+          <td class="label">
+            <label tal:attributes="for widget/name">
+              <span class="required" tal:condition="widget/required">*</span>
+              <span i18n:translate="" tal:content="widget/label">label</span>:
+            </label>
+          </td>
+          <td class="field">
+            <span class="widget" 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">
+              <span class="hint" tal:content="structure widget/hint">hint</span>
+            </tal:hint>
+          </td>
+        </tr>
+      </tal:block>
+    </tbody>
+  </table>
+  <div class="actionButtons" tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/departmentmanagepage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/departmentmanagepage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/departmentmanagepage.pt	(revision 6703)
@@ -0,0 +1,161 @@
+<h2 i18n:translate="" tal:condition="view/label" tal:content="view/label">Label</h2>
+<div class="form-status"  tal:define="status view/status"  tal:condition="status"> Form Status:
+  <span i18n:translate="" tal:content="view/status"> Form status summary
+  </span>
+</div>
+<form action="." tal:attributes="action request/URL" method="POST" class="edit-form" enctype="multipart/form-data">
+  <div id="tabs">
+    <ul>
+      <li>
+      <a href="#tab-1">
+        <span>Settings
+        </span></a>
+      </li>
+      <li>
+      <a href="#tab-2">Courses</a>
+      </li>
+      <li>
+      <a href="#tab-3">Certificates</a>
+      </li>
+      <li>
+      <a href="#tab-4">Local Roles</a>
+      </li>
+    </ul>
+    <div id="tab-1">
+      <table class="zebra">
+        <tbody>
+          <tal:block repeat="widget view/widgets">
+            <tr>
+              <td class="label">
+                <label tal:attributes="for widget/name">
+                  <span class="required" tal:condition="widget/required">*
+                  </span>
+                  <span i18n:translate="" tal:content="widget/label">label
+                  </span>:
+                </label> </td>
+              <td class="field">
+                <span class="widget" 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">
+                  <span class="hint" tal:content="structure widget/hint">hint
+                  </span>
+                </tal:hint> </td>
+            </tr>
+          </tal:block>
+        </tbody>
+      </table>
+      <div class="actionButtons" 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>Code
+            </th>
+            <th>Title
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getCourses" class="gradeB"> <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 class="actionButtons" 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"> <br />
+      <table class="display dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th>Code
+            </th>
+            <th>Title
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getCertificates" class="gradeB"> <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 class="actionButtons" 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"> <br />
+      <table class="display dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th>User Id
+            </th>
+            <th>Name
+            </th>
+            <th>Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles" class="gradeB"> <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 class="actionButtons" 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="zebra">
+        <tr> <td>
+            <select id="user" name="user">
+              <option tal:attributes="value user/name" tal:repeat="user view/getUsers">
+              <span tal:replace="user/val/description">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/uli-studentpw/src/waeup/sirp/browser/templates/departmentpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/departmentpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/departmentpage.pt	(revision 6703)
@@ -0,0 +1,59 @@
+<h2 i18n:translate=""
+tal:condition="view/label"
+tal:content="view/label">Label</h2>
+
+<div id="tabs">
+    <ul>
+      <li><a href="#tab-1"><span>Courses</span></a></li>
+      <li><a href="#tab-2"><span>Certificates</span></a></li>
+    </ul>
+    
+    <div id="tab-1"> 
+    <br /> 
+    <table class="display dataTable">
+      <thead>
+        <tr>
+          <th>Code</th><th>Title</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="entry view/getCourses"
+    	class="gradeB">
+          <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>Code</th><th>Title</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="entry view/getCertificates"
+    	class="gradeB">
+          <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>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/exception.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/exception.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/exception.pt	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/browser/templates/exportxmlaction.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/exportxmlaction.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/exportxmlaction.pt	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/browser/templates/facultycontainermanagepage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/facultycontainermanagepage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/facultycontainermanagepage.pt	(revision 6703)
@@ -0,0 +1,63 @@
+<h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+<div class="form-status"
+    tal:define="status view/status"
+    tal:condition="status">
+    Form Status:
+    <span i18n:translate="" tal:content="view/status">
+      Form status summary
+    </span>
+</div>
+
+<form action="." tal:attributes="action request/URL" method="POST"
+      class="edit-form" enctype="multipart/form-data">
+
+  <div id="tabs">
+    <ul>
+      <li><a href="#tab-1">
+        <span tal:content="view/subunits">Contents</span>
+      </a></li>
+    </ul>
+
+    <div id="tab-1">
+      <h3 tal:content="view/subunits">Faculties</h3>
+      <table class="zebra">
+        <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 class="actionButtons" 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>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/facultymanagepage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/facultymanagepage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/facultymanagepage.pt	(revision 6703)
@@ -0,0 +1,138 @@
+<h2 i18n:translate="" tal:condition="view/label" tal:content="view/label">Label</h2>
+<div class="form-status" tal:define="status view/status" tal:condition="status"> Form Status:
+  <span i18n:translate="" tal:content="view/status"> Form status summary
+  </span>
+</div>
+<form action="." tal:attributes="action request/URL" method="POST" class="edit-form" enctype="multipart/form-data">
+  <div id="tabs">
+    <ul>
+      <li>
+      <a href="#tab-1">
+        <span>Settings
+        </span></a>
+      </li>
+      <li>
+      <a href="#tab-2">
+        <span tal:content="view/subunits">Contents
+        </span> </a>
+      </li>
+      <li>
+      <a href="#tab-3">
+        <span>Local Roles
+        </span></a>
+      </li>
+    </ul>
+    <div id="tab-1">
+      <table class="zebra">
+        <tbody>
+          <tal:block repeat="widget view/widgets">
+            <tr>
+              <td class="label">
+                <label tal:attributes="for widget/name">
+                  <span class="required" tal:condition="widget/required">*
+                  </span>
+                  <span i18n:translate="" tal:content="widget/label">label
+                  </span>:
+                </label> </td>
+              <td class="field">
+                <span class="widget" 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">
+                  <span class="hint" tal:content="structure widget/hint">hint
+                  </span>
+                </tal:hint> </td>
+            </tr>
+          </tal:block>
+        </tbody>
+      </table>
+      <div class="actionButtons" 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">
+      <h3 tal:content="view/subunits">Departments</h3>
+      <table class="zebra">
+        <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 class="actionButtons" 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"> <br />
+      <table class="display dataTableManage">
+        <thead>
+          <tr>
+            <th>&nbsp;
+            </th>
+            <th>User Id
+            </th>
+            <th>Name
+            </th>
+            <th>Local Role
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr tal:repeat="entry view/getUsersWithLocalRoles" class="gradeB"> <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 class="actionButtons" 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="zebra">
+        <tr> <td>
+            <select id="user" name="user">
+              <option tal:repeat="user view/getUsers" tal:attributes="value user/name">
+              <span tal:replace="user/val/description">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/uli-studentpw/src/waeup/sirp/browser/templates/facultypage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/facultypage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/facultypage.pt	(revision 6703)
@@ -0,0 +1,20 @@
+<h2 tal:content="view/label">TITLE</h2>
+
+<div tal:condition="python: not len(context.keys())">
+There no subobjects registered yet.
+</div>
+
+<table class="zebra">
+  <thead>
+    <tr>
+      <th>Code</th>
+      <th>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/uli-studentpw/src/waeup/sirp/browser/templates/importxmlaction.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/importxmlaction.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/importxmlaction.pt	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/browser/templates/importxmlpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/importxmlpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/importxmlpage.pt	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/browser/templates/loginpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/loginpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/loginpage.pt	(revision 6703)
@@ -0,0 +1,29 @@
+<h2 i18n:translate=""  tal:condition="view/label"  tal:content="view/label">Label</h2>
+<form method="post">
+  <table id="login" class="form"   summary="Table for entering login information">
+    <tbody>
+      <tr>
+        <th>
+          <label for="form.login">User Name or Student Id
+          </label>
+        </th> <td>
+          <input type="text" id="login" name="form.login" size="20" /> </td>
+      </tr>
+      <tr>
+        <th>
+          <label for="form.password">Password
+          </label>
+        </th><td>
+          <input type="password" id="password" name="form.password"  size="20" value="" />
+          &nbsp;&nbsp;
+          <font color="red"> Notice: Passwords are case sensitive.
+          </font></td>
+      </tr>
+    </tbody>
+  </table>
+  <input type="hidden" name="camefrom"   tal:attributes="value view/camefrom" />
+  <input type="submit" name="SUBMIT" value="Login" /><br /><br />
+  <p>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>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/logoutpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/logoutpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/logoutpage.pt	(revision 6703)
@@ -0,0 +1,2 @@
+<div>
+</div>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/messages.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/messages.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/messages.pt	(revision 6703)
@@ -0,0 +1,7 @@
+<ul>
+  <li
+    tal:repeat="message view/messages"
+    tal:attributes="class message/type"
+    tal:content="structure message/message">Sauron will arrive soon!
+  </li>
+</ul>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/notfound.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/notfound.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/notfound.pt	(revision 6703)
@@ -0,0 +1,15 @@
+<h2>
+  The page you are trying to access is not available
+</h2>
+
+<div class="error"><b>Please note the following:</b></div>
+<div>
+  <ol class="list">
+    <li>
+      You might have misspelled the URL
+    </li>
+    <li>
+        You might be trying to access a non-existing page
+    </li>
+  </ol>
+</div>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/plainactionbutton.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/plainactionbutton.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/plainactionbutton.pt	(revision 6703)
@@ -0,0 +1,7 @@
+<span class="yui-button">
+  <span class="first-child">
+    <a href=""
+       tal:attributes="href viewlet/target_url">
+    <span tal:content="viewlet/text">TEXT</span></a>
+  </span>
+</span>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/primarynavtab.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/primarynavtab.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/primarynavtab.pt	(revision 6703)
@@ -0,0 +1,7 @@
+<li tal:attributes="class viewlet/active">
+  <a href="#"
+     tal:attributes="href viewlet/link_target"
+     tal:content="viewlet/tab_title">
+    Primary Tab Title
+  </a>
+</li>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/removesubobjectspage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/removesubobjectspage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/removesubobjectspage.pt	(revision 6703)
@@ -0,0 +1,32 @@
+  <div tal:condition="python: not len(context.keys())">
+    There are no subobjects registered yet.
+  </div>
+  <form method="POST" tal:condition="python: len(context.keys())">
+    <div tal:condition="python: not len(context.keys())">
+    There are no faculties registered yet.
+    </div>
+    
+    <table>
+      <thead>
+        <tr>
+          <th>&nbsp;</th>
+          <th>Code</th>
+          <th>Title</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr tal:repeat="value context/values"
+    	tal:attributes="class python: repeat['value'].odd() and 'odd' or 'even'">
+    	  <td>
+    	  <input type="checkbox"
+    		 name="val_id"
+    		 tal:attributes="value value/__name__" />
+    	  </td>    	
+          <td> <a tal:attributes="href value/__name__"> <span tal:content="value/code">Name</span></a></td>
+          <td tal:content="value/title">Title</td>
+        </tr>
+      </tbody>
+    </table>
+    <input type="submit" name="REMOVE" value="Remove selected" onclick="return confirmPost('All sububjects will be removed too. Are you sure?')"/>
+    <input type="submit" name="CANCEL" value="Cancel" />
+  </form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/searchpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/searchpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/searchpage.pt	(revision 6703)
@@ -0,0 +1,35 @@
+<h2 i18n:translate="" tal:condition="view/label" tal:content="view/label">Label</h2>
+<form method="post">
+  <input type="text" name="query"
+	 tal:attributes="value view/query" />
+    <input type="submit" name="SUBMIT" value="Search" />
+</form>
+<br />
+<span tal:condition="view/hitlist">
+    <h3>Search Results</h3>
+    <table class="display dataTable">
+        <thead>
+            <tr>
+                <th>Code</th>
+                <th>Title</th>
+                <th>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">
+                Type
+            </td>
+            </tr>
+        </tbody>
+    </table>
+    <div class="actionButtons">
+    </div>
+</span>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/staffsitelayout.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/staffsitelayout.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/staffsitelayout.pt	(revision 6703)
@@ -0,0 +1,114 @@
+<!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">
+  <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 class="rounded yui-skin-sam">
+    <div id="doc3" class="yui-t1">
+      <div id="hd" role="banner">
+        <div id="hd">
+          <h1 id="logo">
+            <a href="" tal:attributes="href view/application_url">
+              <span class="uni-logo">
+              </span> </a>
+            <span id="uni-title" tal:content="layout/getAppTitle"> Sample University
+            </span> </h1>
+          <div id="navigation" class="yui-navset" style="text-align:right;">
+            <ul id="primary-navigation">
+              <tal:primary_nav content="structure provider:primary_nav" />
+            </ul>
+            <ul id="user-navigation">
+              <li tal:condition="not: layout/isAuthenticated">
+              <a href="#" tal:attributes="href python: view.url(layout.site, 'login')">
+                  Login
+              </a>
+              </li>
+              <li tal:condition="layout/isAuthenticated">
+              <a href="#" tal:attributes="href python: view.url(layout.site, '@@contactadmin')">
+                  Contact
+              </a>
+              </li>
+              <li tal:condition="layout/isAuthenticated">
+              <a href="#" tal:attributes="href python: view.url(context, '@@logout')">Logout</a>
+              </li>
+            </ul>
+            <div class="clear">
+            </div>
+          </div>
+        </div>
+      </div>
+      <div id="bd" role="main">
+        <span tal:condition="python: layout.isAuthenticated()"
+              tal:content="structure provider:breadcrumbs" />
+
+        <div class="yui-b">
+          <div class="block" tal:condition="layout/getStudentName">
+            <div class="hd">
+              <h2 tal:content="layout/getStudentName">Student name</h2>
+            </div>
+            <div class="bd">
+              <tal:left content="structure provider:left_studentmanage" />
+            </div>
+          </div>
+          <div class="block" tal:condition="layout/isAuthenticated">
+            <div class="hd">
+              <h2 tal:content="layout/getUserTitle">Username</h2>
+            </div>
+            <div class="bd">
+              <tal:left content="structure provider:left" />
+            </div>
+          </div>
+        </div>
+
+        <div id="yui-main">
+          <div class="yui-b">
+            <div class="yui-g">
+              <div class="block">
+                <div class="hd" tal:condition="python: layout.isAuthenticated()">
+                  <span tal:content="view/title"> THE CONTENT TITLE </span>
+                  <span tal:condition="layout/getStudentName" class="wfstatus">
+                  Sudent Id: <span tal:content="python:context.getStudent().student_id">Student Id</span>
+                  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+                  Registration State: <span tal:content="python:context.getStudent().state">State</span>
+                  </span>
+                </div>
+                <div class="bd">
+                  <div class="actionbar">
+                    <tal:actionbar content="structure provider:actionbar" />
+                  </div>
+                  <div class="alert warning ui-state-error"
+                      tal:define="message context/@@messages"
+                      tal:condition="python: len(message)>11">
+                    <span class="ui-icon ui-icon-alert">A
+                    </span>
+                    <span tal:content="structure message">
+                    </span>
+                  </div>
+                  <div tal:replace="structure view/content"> THE CONTENT
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+      </div>
+      <div id="ft" role="contentinfo">
+        <p>Student Information and Registration Portal<br />
+            Copyright &copy; WAeUP Group, 2008-2011
+        </p>
+      </div>
+    </div>
+  </body>
+</html>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/studentsitelayout.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/studentsitelayout.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/studentsitelayout.pt	(revision 6703)
@@ -0,0 +1,99 @@
+<!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">
+  <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 class="rounded yui-skin-sam">
+    <div id="doc3" class="yui-t7">
+      <div id="hd" role="banner">
+        <div id="hd">
+          <h1 id="logo">
+            <a href="" tal:attributes="href view/application_url">
+              <span class="uni-logo">
+              </span> </a>
+            <span id="uni-title" tal:content="layout/getAppTitle"> Sample University
+            </span> </h1>
+          <div id="navigation" class="yui-navset" style="text-align:right;">
+            <ul id="primary-navigation" tal:condition="layout/isAuthenticated">
+              <tal:primary_nav content="structure provider:primary_nav_student" />
+            </ul>
+            <ul id="primary-navigation" tal:condition="not: layout/isAuthenticated">
+              <tal:primary_nav content="structure provider:primary_nav" />
+            </ul>
+            <ul id="user-navigation">
+              <li tal:condition="not: layout/isAuthenticated">
+              <a href="#" tal:attributes="href python: view.url(layout.site, 'login')">
+                  Login
+              </a>
+              </li>
+              <li tal:condition="layout/isAuthenticated">
+              <a href="#" tal:attributes="href python: view.url(layout.site, '@@contactadmin')">
+                  Contact
+              </a>
+              </li>
+              <li tal:condition="layout/isAuthenticated">
+              <a href="#" tal:attributes="href python: view.url(context, '@@logout')">Logout</a>
+              </li>
+            </ul>
+            <div class="clear">
+            </div>
+          </div>
+        </div>
+      </div>
+      <div id="bd" role="main">
+
+        <div class="yui-b">
+          <div class="block" tal:condition="layout/getStudentName">
+            <div class="studentmenu">
+              <tal:left content="structure provider:top_student" />
+            </div>
+          </div>
+        </div>
+
+        <div id="yui-main">
+          <div class="yui-b">
+            <div class="yui-g">
+              <div class="block">
+                <div class="bd">
+                  <div class="actionbar">
+                    <tal:actionbar content="structure provider:actionbar" />
+                  </div>
+                  <div class="alert warning ui-state-error"
+                      tal:define="message context/@@messages"
+                      tal:condition="python: len(message)>11">
+                    <span class="ui-icon ui-icon-alert">A
+                    </span>
+                    <span tal:content="structure message">
+                    </span>
+                  </div>
+                  <div tal:replace="structure view/content"> THE CONTENT
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+      </div>
+      <div id="ft" role="contentinfo">
+        <p>Student Information and Registration Portal<br />
+            Copyright &copy; WAeUP Group, 2008-2011
+        </p>
+      </div>
+    </div>
+  </body>
+</html>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/subobjectlist.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/subobjectlist.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/subobjectlist.pt	(revision 6703)
@@ -0,0 +1,19 @@
+<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"
+	tal:attributes="class python: repeat['row'].odd() and 'odd' or 'even'">
+      <td tal:repeat="viewlet row">
+	<div tal:replace="structure viewlet/render">
+	  viewlet
+	</div>
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/unauthorized.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/unauthorized.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/unauthorized.pt	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/browser/templates/universitypage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/universitypage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/universitypage.pt	(revision 6703)
@@ -0,0 +1,3 @@
+<h2 tal:content="view/label">Title</h2>
+
+<span tal:replace="structure view/widgets/frontpage">Frontpage</span>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/universityrss20feed.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/universityrss20feed.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/universityrss20feed.pt	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/browser/templates/usercontainerpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/usercontainerpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/templates/usercontainerpage.pt	(revision 6703)
@@ -0,0 +1,35 @@
+<h2>Portal Users</h2>
+
+<p>
+  Here you can manage portal users.  
+</p>
+
+<table class="zebra">
+  <thead>
+    <tr>
+      <th>User ID</th>
+      <th>Name</th>
+      <th>Description</th>
+      <th>Global roles</th>
+      <th>Local roles</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr tal:repeat="account context/values">
+      <td tal:content="account/name">User name</td>
+      <td tal:content="account/title">Title</td>
+      <td tal:content="account/description">Description</td>
+      <td tal:content="structure python:view.getGlobalRoles(account)">Global 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 type="submit" name="edit" value="edit" />
+            <input type="submit" name="delete" value="delete" />
+        </form>
+      </td>
+    </tr>
+  </tbody>
+</table>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/tests/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/tests/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/tests/__init__.py	(revision 6703)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/tests/test_permissions.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/tests/test_permissions.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/tests/test_permissions.py	(revision 6703)
@@ -0,0 +1,127 @@
+##
+## test_batching.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Aug 24 02:04:44 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
+##
+"""
+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/@@index',
+    '/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/uli-studentpw/src/waeup/sirp/browser/theming.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/theming.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/theming.py	(revision 6703)
@@ -0,0 +1,110 @@
+##
+## theming.py
+## Login : <uli@pu.smp.net>
+## Started on  Thu Apr 21 19:50:04 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
+##
+"""
+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 SimpleWAeUPVocabulary
+from waeup.sirp.browser.interfaces import ITheme
+from waeup.sirp.browser.resources import (
+    waeuptheme_red1, waeuptheme_gray1, waeuptheme_base,
+    )
+
+class WAeUPThemeRed1(grok.GlobalUtility):
+    grok.implements(ITheme)
+    grok.name('red waeup theme')
+
+    description = u"Uli's Red Theme"
+
+    def getResources(self):
+        return [waeuptheme_red1]
+
+class WAeUPThemeGray1(grok.GlobalUtility):
+    grok.implements(ITheme)
+    grok.name('gray waeup theme')
+
+    description = u"Henrik's Gray Theme"
+
+    def getResources(self):
+        return [waeuptheme_gray1]
+
+class WAeUPThemeBase(grok.GlobalUtility):
+    """A theme based on jQuery only.
+
+    XXX: crappy yet.
+    """
+    grok.implements(ITheme)
+    grok.name('base theme')
+    description = u'Base Theme'
+
+    def getResources(self):
+        return [waeuptheme_base]
+
+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 = SimpleWAeUPVocabulary(*terms)
+        return vocab
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/viewlets.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/viewlets.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/browser/viewlets.py	(revision 6703)
@@ -0,0 +1,719 @@
+# -*- coding: utf-8 -*-
+
+import grok
+
+from zope.component import getMultiAdapter, queryAdapter
+from zope.interface import Interface
+from zope.location.interfaces import ISite
+from waeup.sirp.browser.pages import (
+    UniversityPage, FacultyContainerPage, DatacenterPage, FacultyPage,
+    DepartmentPage, CoursePage, CertificatePage, CertificateCoursePage, UserContainerPage)
+from waeup.sirp.browser.interfaces import (
+    IFacultyContainer, IFaculty, IDepartment, ICourse, ICertificate,
+    ICertificateCourse, IBreadcrumbContainer, IUniversity, IUserContainer)
+from waeup.sirp.interfaces import (IWAeUPObject, IWAeUPXMLExporter,
+                                   IWAeUPXMLImporter, IDataCenter)
+from waeup.sirp.browser.layout import WAeUPPage
+
+grok.templatedir('templates')
+grok.context(IWAeUPObject) # Make IWAeUPObject the default context
+
+class LeftSidebar(grok.ViewletManager):
+    grok.name('left')
+
+class BreadCrumbManager(grok.ViewletManager):
+    grok.name('breadcrumbs')
+
+class ActionBar(grok.ViewletManager):
+    grok.name('actionbar')
+
+class AdministrationTasks(grok.ViewletManager):
+    grok.name('admintasks')
+
+
+#
+# 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(IWAeUPObject)
+    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(IWAeUPObject)
+    grok.viewletmanager(BreadCrumbManager)
+    grok.order(1)
+
+    def getEntries(self):
+        result = []
+        site = grok.getSite()
+        context = self.context
+        breadcrumbs = IBreadcrumbContainer(self.view)
+        for breadcrumb in breadcrumbs:
+            yield dict(
+                title = breadcrumb.title,
+                url = self.view.url(breadcrumb.context, breadcrumb.viewname)
+                )
+
+
+# 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(IWAeUPObject)
+    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'<div class="portlet"><a href="%s"><img src="%s" /> %s </a></div>' % (
+                    self.url, self.icon_url, self.title)
+            else:
+                return u'<div class="portlet"><a href="%s">%s </a></div>' % (
+                    self.url, self.title)
+        else:
+            return ''
+            
+#
+# waeup.sirp.app.University viewlets...
+#
+class Login(grok.Viewlet):
+    """This viewlet allows to login in the sidebar.
+    """
+    grok.viewletmanager(LeftSidebar)
+    grok.context(IWAeUPObject)
+    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'<div class="portlet"><a href="%s">%s</a></div>' % (
+                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(IWAeUPObject)
+    grok.view(Interface)
+    grok.order(5)
+    # This link is only displayed, if the user is
+    # allowed to use it!
+    grok.require('waeup.managePortalSettings')
+
+    link = '@@manage'
+    text = u'Portal Settings'
+    
+    def render(self):
+        url = self.view.url(grok.getSite(), self.link)
+        return u'<div class="portlet"><a href="%s">%s</a></div>' % (
+                url, self.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 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'
+
+#
+# Manage ("Edit settings") actions...
+#
+class ManageUniversityActionButton(ManageActionButton):
+    """ 'Edit' button for University instances.
+    """
+    grok.context(IUniversity)
+    grok.view(UniversityPage)
+    text = 'Edit portal settings'
+    
+#class ManageFacultiesActionButton(ManageActionButton):
+#    """ 'Edit settings' button for faculty container.
+#    """
+#    grok.context(IFacultyContainer)
+#    grok.view(FacultyContainerPage)
+
+class ManageDataCenterActionButton(ManageActionButton):
+    """ 'Edit settings' button for datacenter.
+    """
+    grok.context(IDataCenter)
+    grok.view(DatacenterPage)
+    text = 'Edit settings'
+
+class ManageFacultyContainerActionButton(ManageActionButton):
+    """ 'Manage settings' button for faculties.
+    """
+    grok.context(IFacultyContainer)
+    grok.view(FacultyContainerPage)
+    text = 'Manage academic section'
+
+class SearchFacultyContainerActionButton(ManageActionButton):
+    """ 'Manage settings' button for faculties.
+    """
+    grok.context(IFacultyContainer)
+    grok.view(FacultyContainerPage)
+    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 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 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(IUserContainer)
+    grok.view(UserContainerPage)
+    text = 'Add user'
+
+#class AddFacultyActionButton(AddActionButton):
+#    grok.context(IFacultyContainer)
+#    grok.view(FacultyContainerPage)
+#    text = 'Add faculty'
+    
+#class AddDepartmentActionButton(AddActionButton):
+#    grok.context(IFaculty)
+#    grok.view(FacultyPage)
+#    text = 'Add department'
+
+#class AddCertificateActionButton(AddActionButton):
+#    grok.context(IDepartment)
+#    grok.view(DepartmentPage)
+#    grok.order(3)
+#    text = 'Add certificate'
+#    target = 'addcertificate'
+    
+#class AddCourseActionButton(AddActionButton):
+#    grok.context(IDepartment)
+#    grok.view(DepartmentPage)
+#    grok.order(4)
+#    text = 'Add course'
+#    target = 'addcourse'
+
+#class AddCertificateCourseActionButton(AddActionButton):
+#    grok.context(ICertificate)
+#    grok.view(CertificatePage)
+#    grok.order(4)
+#    text = 'Add course referrer'
+#    target = 'addcertificatecourse'
+
+#
+# Remove actions...
+#
+
+
+#class RemoveFacultyActionButton(RemoveActionButton):
+#    grok.context(IFacultyContainer)
+#    grok.view(FacultyContainerPage)
+#    text = 'Remove faculty'
+   
+#class RemoveDepartmentActionButton(RemoveActionButton):
+#    grok.context(IFaculty)
+#    grok.view(FacultyPage)
+#    text = 'Remove department'
+    
+
+#
+# Actions with a 'browse' icon...
+#
+class BrowseActionButton(ActionButton):
+    grok.baseclass()
+    grok.context(IWAeUPObject)
+    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.View')
+
+    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')
+    grok.template('primarynavtab')
+
+    pnav = 0
+    tab_title = u'Home'
+
+class FacultiesTab(PrimaryNavTab):
+    """Faculties-tab in primary navigation.
+    """
+    grok.order(2)
+    grok.require('waeup.View')
+    grok.template('primarynavtab')
+
+    pnav = 1
+    tab_title = u'Academics'
+
+    @property
+    def link_target(self):
+        return self.view.application_url('faculties')
+
+    
+class ContactTab(PrimaryNavTab):
+    """Contact tab in primary navigation.
+    """
+    grok.order(4)
+    grok.require('waeup.Anonymous')
+    grok.template('primarynavtab')
+    
+    pnav = 2
+    def tab_title(self):
+        """Display tab only for anonymous. Authenticated users can call the 
+        form from the user navigation bar.
+        """
+        userid = self.request.principal.id
+        if userid != 'zope.anybody':
+            tt = u'' 
+        else:
+            tt = u'Enquiries'         
+        return tt 
+
+    @property
+    def active(self):
+        view_pnav = getattr(self.view, 'pnav', 0)
+        userid = self.request.principal.id
+        if view_pnav == self.pnav and userid == 'zope.anybody':
+            return 'active'
+        return ''
+
+    @property
+    def link_target(self):
+        return self.view.application_url('contactadmin')        
+
+
+
+#
+# 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 AdminTaskPortalSettings(AdminTask):
+    """Entry on administration page that link to portal settings.
+    """
+    grok.order(1)
+    grok.require('waeup.managePortalSettings')
+
+    link_title = 'Portal Settings'
+    def link_target(self):
+        return self.view.url(self.view.context, '@@manage')
+
+class AdminTaskUsers(AdminTask):
+    """Entry on administration page that link to user folder.
+    """
+    grok.order(2)
+    grok.require('waeup.manageUsers')
+
+    link_title = 'Portal Users'
+    target_viewname = 'users'
+    
+class AdminTaskDatacenter(AdminTask):
+    """Entry on administration page that link to datacenter.
+    """
+    grok.order(3)
+    grok.require('waeup.manageDataCenter')
+
+    link_title = 'Data Center'
+    target_viewname = 'datacenter'
+
+class 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 FacultyContainerListHead(grok.Viewlet):
+    """The header line of faculty container subobject lists.
+    """
+    grok.order(1)
+    grok.viewletmanager(SubobjectLister)
+    grok.context(IFacultyContainer)
+    grok.require('waeup.View')
+
+    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.View')
+
+    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.View')
+
+    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/uli-studentpw/src/waeup/sirp/catalog.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/catalog.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/catalog.py	(revision 6703)
@@ -0,0 +1,48 @@
+"""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 WAeUPQuery(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(WAeUPQuery)
+
+# 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/uli-studentpw/src/waeup/sirp/catalog.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/catalog.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/catalog.txt	(revision 6703)
@@ -0,0 +1,235 @@
+:mod:`waeup.sirp.catalog` -- Cataloging support for WAeUP SIRP
+**************************************************************
+
+.. module:: waeup.sirp.catalog
+
+Components that support cataloging and searching objects inside a
+WAeUP SIRP site.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
+
+.. contents::
+
+Classes
+=======
+
+:class:`WAeUPQuery`
+-------------------
+
+.. class:: WAeUPQuery()
+
+   .. 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, `WAeUPQuery` 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 WAeUPQuery 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.WAeUPQuery 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 WAeUPQuery (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
+``WAeUPQuery``:
+
+    >>> 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/uli-studentpw/src/waeup/sirp/configure.zcml
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/configure.zcml	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/configure.zcml	(revision 6703)
@@ -0,0 +1,8 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:grok="http://namespaces.zope.org/grok">
+  <include package="grok" />
+  <include package="waeup.sirp" file="meta.zcml" />
+  <includeDependencies package="." />
+  <grok:grok package="." />
+  <includeOverrides package="waeup.sirp.utils" file="overrides.zcml" />
+</configure>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/datacenter.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/datacenter.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/datacenter.py	(revision 6703)
@@ -0,0 +1,259 @@
+"""WAeUP 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.interfaces import ObjectEvent
+from waeup.sirp.interfaces import (IDataCenter, IDataCenterFile,
+                                   IDataCenterStorageMovedEvent)
+from waeup.sirp.utils.helpers import copyFileSystemTree
+from waeup.sirp.utils.logger import Logger
+
+class DataCenter(grok.Container, Logger):
+    """A data center contains CSV files.
+    """
+    grok.implements(IDataCenter)
+    storage = os.path.join(os.path.dirname(__file__), 'files')
+
+    logger_name = 'waeup.sirp.${sitename}.datacenter'
+    logger_filename = 'datacenter.log'
+
+    def __init__(self, *args, **kw):
+        super(DataCenter, self).__init__(*args, **kw)
+        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 = copyFileSystemTree(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('%c')
+
+    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/uli-studentpw/src/waeup/sirp/datacenter.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/datacenter.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/datacenter.txt	(revision 6703)
@@ -0,0 +1,259 @@
+WAeUP Data Center
+*****************
+
+The WAeUP data center cares for managing CSV files and importing then.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
+
+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
+    '/.../waeup/sirp/files'
+
+
+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/uli-studentpw/src/waeup/sirp/frontpage.rst
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/frontpage.rst	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/frontpage.rst	(revision 6703)
@@ -0,0 +1,31 @@
+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
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/ftesting.zcml
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/ftesting.zcml	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/ftesting.zcml	(revision 6703)
@@ -0,0 +1,62 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   i18n_domain="waeup.sirp"
+   package="waeup.sirp"
+   >
+
+  <include package="grok" />
+  <includeOverrides package="waeup.sirp" />
+
+  <!-- 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/uli-studentpw/src/waeup/sirp/hostel/hostel.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/hostel/hostel.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/hostel/hostel.py	(revision 6703)
@@ -0,0 +1,13 @@
+import grok
+from waeup.sirp.interfaces import IHostel
+
+class Hostel(grok.Container):
+    """This is a hostel record.
+    """    
+    grok.implements(IHostel)
+ 
+    def __init__(self, name=u'Unnamed Hostel', **kw):
+        super(Hostel, self).__init__(**kw)
+        self.name = name
+
+grok.global_utility(Hostel, provides=IHostel)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/hostel/hostelcontainer.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/hostel/hostelcontainer.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/hostel/hostelcontainer.py	(revision 6703)
@@ -0,0 +1,36 @@
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.sirp.interfaces import IHostelContainer, IHostel
+
+class HostelContainer(grok.Container):
+
+    grok.implements(IHostelContainer)
+    grok.require('waeup.manageUniversity')
+
+    # A simple counter for ids.
+    next_id = 0L
+
+    def addHostel(self, hostel):
+        if not IHostel.providedBy(hostel):
+            raise TypeError('HostelContainers contain only IHostel instances')
+        id = str(self.next_id)
+        self[id] = hostel
+        while self.next_id in self.keys():
+            # Look for next unused int...
+            self.next_id += 1
+        return id
+
+class HostelContainerFactory(grok.GlobalUtility):
+    """A factory for faculty containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.HostelContainer')
+    title = u"Create a new hostel container.",
+    description = u"This factory instantiates new hostel containers."
+
+    def __call__(self):
+        return HostelContainer()
+
+    def getInterfaces(self):
+        return implementedBy(HostelContainer)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/hostel/hostelcontainer.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/hostel/hostelcontainer.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/hostel/hostelcontainer.txt	(revision 6703)
@@ -0,0 +1,67 @@
+waeup.sirp.hostel.HostelContainer
+*********************************
+
+Containers for hostels.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
+
+Getting a hostel container
+==========================
+
+We can easily create `HostelContainers`::
+
+    >>> from waeup.sirp.hostel.hostelcontainer import HostelContainer
+    >>> mycontainer = HostelContainer()
+
+Hostel containers provide ``IHostelContainer``:
+
+    >>> from waeup.sirp.interfaces import IHostelContainer
+    >>> IHostelContainer.providedBy(mycontainer)
+    True
+
+Another way to get a hostel container -- without importing the class
+-- is via factories. We registered a factory for hostel containers
+under the name ``waeup.HostelContainer``. So we can ask for an object
+by calling the appropriate factory:
+
+    >>> from zope.component import createObject
+    >>> createObject(u'waeup.HostelContainer')
+    <waeup.sirp.hostel.hostelcontainer.HostelContainer object at 0x...>
+
+This way we get a thing that implements IHostelContainer without
+imports or similar.
+
+We can be sure, that the full interface is supported by the
+HostelContainer class::
+
+    >>> from zope.interface.verify import verifyClass
+    >>> verifyClass(IHostelContainer, HostelContainer)
+    True
+
+
+Storing things in hostel containers
+====================================
+
+We can, of course, store things in a hostel container. But when we
+really store an object, then it must be a hostel::
+
+    >>> mycontainer.addHostel(42)
+    Traceback (most recent call last):
+    ...
+    TypeError: HostelContainers contain only IHostel instances
+
+Okay, so we have to get a hostel first::
+
+    >>> from waeup.sirp.hostel.hostel import Hostel
+    >>> myhostel = Hostel()
+
+We can add this hostel to our container::
+
+    >>> mycontainer.addHostel(myhostel)
+    '0'
+
+We get back the key, under which the hostel 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/uli-studentpw/src/waeup/sirp/image/README.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/README.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/README.txt	(revision 6703)
@@ -0,0 +1,216 @@
+waeup.sirp.image -- handling image files
+========================================
+
+The image file widget is built on top of the :class:`WAeUPImageFile` object::
+
+  >>> from waeup.sirp.image import WAeUPImageFile
+  >>> file = WAeUPImageFile('foo.jpg', 'mydata')
+  >>> file.filename
+  'foo.jpg'
+  >>> file.data
+  'mydata'
+  >>> file.size
+  6
+  >>> f = file.file
+  >>> f.read()
+  'mydata'
+
+We can also create WAeUPImageFile 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 WAeUPImageFile 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 = WAeUPImageFile('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::
+
+  >>> file.size
+  9L
+
+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 = WAeUPImageFile('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.WAeUPImageFile'>, 'foo')
+
+which means: `ImageFile` fields should better contain
+:class:`WAeUPImageFile` instances.
+
+We can store normal :class:`WAeUPImageFile` instances:
+
+  >>> field.validate(WAeUPImageFile('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(
+  ...     WAeUPImageFile('bar.jpg', '123456789012')) is None
+  True
+
+  >>> field.validate(
+  ...     WAeUPImageFile('bar.jpg', '12345')) is None
+  True
+
+the following are not:
+
+  >>> field.validate(
+  ...     WAeUPImageFile('bar.jpg', '1234567890123'))
+  Traceback (most recent call last):
+  ...
+  TooBig: ('bar.jpg', '13 bytes (max: 12 bytes)')
+
+  >>> field.validate(
+  ...     WAeUPImageFile('bar.jpg', '1234'))
+  Traceback (most recent call last):
+  ...
+  TooSmall: ('bar.jpg', '4 bytes (min: 5 bytes)')
+
+
+Broken Unequality Comparison
+----------------------------
+
+WAeUPImageFile does not reproduce the broken unequal comparison from
+its base:
+
+  >>> f1 = WAeUPImageFile('bar.jpg', '123456789')
+  >>> f2 = WAeUPImageFile('bar.jpg', '123456789')
+  >>> f3 = WAeUPImageFile('baz.jpg', '1234')
+  >>> f1 == f2
+  True
+
+  >>> f1 != f2
+  False
+
+  >>> f1 == f3
+  False
+
+  >>> f1 != f3
+  True
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/__init__.py	(revision 6703)
@@ -0,0 +1,10 @@
+"""Image file components.
+
+Includings schemas, widgets and the content components.
+"""
+from waeup.sirp.image.image import WAeUPImageFile, createWAeUPImageFile
+
+__all__ = [
+    "WAeUPImageFile",
+    "createWAeUPImageFile",
+    ]
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/__init__.py	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/image/browser/tests/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/tests/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/tests/__init__.py	(revision 6703)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/tests/image.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/tests/image.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/tests/image.txt	(revision 6703)
@@ -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 WAeUPImageFile
+  >>> testimage = os.path.join(os.path.dirname(__file__), 'sample.jpg')
+  >>> testimage2 = os.path.join(os.path.dirname(__file__), 'sample2.jpg')
+  >>> some_file = WAeUPImageFile('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 = WAeUPImageFile('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.WAeUPImageFile` 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 = WAeUPImageFile(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 WAeUPImageFile object with that
+file_id stored:
+
+  >>> from waeup.sirp.image import createWAeUPImageFile
+  >>> image = createWAeUPImageFile(
+  ...     '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 createWAeUPImageFile 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/uli-studentpw/src/waeup/sirp/image/browser/tests/test_image.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/tests/test_image.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/tests/test_image.py	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/image/browser/tests/test_views.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/tests/test_views.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/tests/test_views.py	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/image/browser/views.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/views.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/views.py	(revision 6703)
@@ -0,0 +1,52 @@
+##
+## views.py
+## Login : <uli@pu.smp.net>
+## Started on  Sat Sep 18 14:16:06 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
+##
+"""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/uli-studentpw/src/waeup/sirp/image/browser/widget.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/widget.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/browser/widget.py	(revision 6703)
@@ -0,0 +1,170 @@
+##
+## widget.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Sep 14 11:00:16 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
+##
+"""Image file widgets.
+"""
+import os
+from waeup.sirp.image import WAeUPImageFile
+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 WAeUPImageFile(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 WAeUPImageFile(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/uli-studentpw/src/waeup/sirp/image/image.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/image.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/image.py	(revision 6703)
@@ -0,0 +1,53 @@
+##
+## image.py
+## Login : <uli@pu.smp.net>
+## Started on  Mon Apr 25 12:32:42 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
+##
+"""
+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 IWAeUPImageFile
+
+class WAeUPImageFile(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:`WAeUPImageFile` you should use
+    :func:`createWAeUPImageFile`.
+    """
+    implements(IWAeUPImageFile)
+
+    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 createWAeUPImageFile(filename, f):
+    retrieval = getUtility(IFileRetrieval)
+    return retrieval.createFile(filename, f)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/interfaces.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/interfaces.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/interfaces.py	(revision 6703)
@@ -0,0 +1,36 @@
+##
+## interfaces.py
+## Login : <uli@pu.smp.net>
+## Started on  Mon Sep 13 10:28:05 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
+##
+"""Interfaces for waeup.sirp.image
+"""
+from hurry.file.interfaces import IFile, IFileRetrieval, IHurryFile
+
+class IImageFile(IFile):
+    """Image file field.
+    """
+    
+class IWAeUPImageFile(IHurryFile):
+    """Image file.
+    """
+    
+class IImageFileRetrieval(IFileRetrieval):
+    """A file retrieval for images.
+    """
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/schema.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/schema.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/schema.py	(revision 6703)
@@ -0,0 +1,88 @@
+##
+## schema.py
+## Login : <uli@pu.smp.net>
+## Started on  Mon Sep 13 10:31:43 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
+##
+"""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 WAeUPImageFile
+
+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 = WAeUPImageFile
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/tests.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/tests.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/image/tests.py	(revision 6703)
@@ -0,0 +1,41 @@
+##
+## tests.py
+## Login : <uli@pu.smp.net>
+## Started on  Mon Apr 25 14:18:37 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
+##
+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/uli-studentpw/src/waeup/sirp/imagestorage.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/imagestorage.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/imagestorage.py	(revision 6703)
@@ -0,0 +1,217 @@
+##
+## imagestorage.py
+## Login : <uli@pu.smp.net>
+## Started on  Mon Jul  4 16:02:14 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
+##
+"""A storage for image files.
+"""
+import grok
+import hashlib
+import os
+import transaction
+import warnings
+from StringIO import StringIO
+from ZODB.blob import Blob
+from persistent import Persistent
+from hurry.file.interfaces import IFileRetrieval
+from waeup.sirp.image import WAeUPImageFile
+from waeup.sirp.utils.helpers import cmp_files
+
+def md5digest(fd):
+    """Get an MD5 hexdigest for the file stored in `fd`.
+
+    `fd`
+      a file object open for reading.
+
+    """
+    return hashlib.md5(fd.read()).hexdigest()
+
+class Basket(grok.Container):
+    """A basket holds a set of image files with same hash.
+    """
+
+    def _del(self):
+        """Remove temporary files associated with local blobs.
+
+        A basket holds files as Blob objects. Unfortunately, if a
+        basket was not committed (put into ZODB), those blobs linger
+        around as real files in some temporary directory and won't be
+        removed.
+
+        This is a helper function to remove all those uncommitted
+        blobs that has to be called explicitly, for instance in tests.
+        """
+        key_list = self.keys()
+        for key in key_list:
+            item = self[key]
+            if getattr(item, '_p_oid', None):
+                # Don't mess around with blobs in ZODB
+                continue
+            fd = item.open('r')
+            name = getattr(fd, 'name', None)
+            fd.close()
+            if name is not None and os.path.exists(name):
+                os.unlink(name)
+            del self[key]
+        return
+
+    def getInternalId(self, fd):
+        """Get the basket-internal id for the file stored in `fd`.
+
+        `fd` must be a file open for reading. If an (byte-wise) equal
+        file can be found in the basket, its internal id (basket id)
+        is returned, ``None`` otherwise.
+        """
+        fd.seek(0)
+        for key, val in self.items():
+            fd_stored = val.open('r')
+            file_len = os.stat(fd_stored.name)[6]
+            if file_len == 0:
+                # Nasty workaround. Blobs seem to suffer from being emptied
+                # accidentally.
+                site = grok.getSite()
+                if site is not None:
+                    site.logger.warn(
+                        'Empty Blob detected: %s' % fd_stored.name)
+                warnings.warn("EMPTY BLOB DETECTED: %s" % fd_stored.name)
+                fd_stored.close()
+                val.open('w').write(fd.read())
+                return key
+            fd_stored.seek(0)
+            if cmp_files(fd, fd_stored):
+                fd_stored.close()
+                return key
+            fd_stored.close()
+        return None
+
+    @property
+    def curr_id(self):
+        """The current basket id.
+
+        An integer number which is not yet in use. If there are
+        already `maxint` entries in the basket, a :exc:`ValueError` is
+        raised. The latter is _highly_ unlikely. It would mean to have
+        more than 2**32 hash collisions, i.e. so many files with the
+        same MD5 sum.
+        """
+        num = 1
+        while True:
+            if str(num) not in self.keys():
+                return str(num)
+            num += 1
+            if num <= 0:
+                name = getattr(self, '__name__', None)
+                raise ValueError('Basket full: %s' % name)
+
+    def storeFile(self, fd, filename):
+        """Store the file in `fd` into the basket.
+
+        The file will be stored in a Blob.
+        """
+        fd.seek(0)
+        internal_id = self.getInternalId(fd) # Moves file pointer!
+        if internal_id is None:
+            internal_id = self.curr_id
+            fd.seek(0)
+            self[internal_id] = Blob()
+            transaction.commit() # Urgently needed to make the Blob
+                                 # persistent. Took me ages to find
+                                 # out that solution, which makes some
+                                 # design flaw in ZODB Blobs likely.
+            self[internal_id].open('w').write(fd.read())
+            fd.seek(0)
+            self._p_changed = True
+        return internal_id
+
+    def retrieveFile(self, basket_id):
+        """Retrieve a file open for reading with basket id `basket_id`.
+
+        If there is no such id, ``None`` is returned. It is the
+        callers responsibility to close the open file.
+        """
+        if basket_id in self.keys():
+            return self[basket_id].open('r')
+        return None
+
+class ImageStorage(grok.Container):
+    """A container for image files.
+    """
+    def _del(self):
+        for basket in self.values():
+            try:
+                basket._del()
+            except:
+                pass
+
+    def storeFile(self, fd, filename):
+        fd.seek(0)
+        digest = md5digest(fd)
+        fd.seek(0)
+        if not digest in self.keys():
+            self[digest] = Basket()
+        basket_id = self[digest].storeFile(fd, filename)
+        full_id = "%s-%s" % (digest, basket_id)
+        return full_id
+
+    def retrieveFile(self, file_id):
+        if not '-' in file_id:
+            return None
+        full_id, basket_id = file_id.split('-', 1)
+        if not full_id in self.keys():
+            return None
+        return self[full_id].retrieveFile(basket_id)
+
+class ImageStorageFileRetrieval(Persistent):
+    grok.implements(IFileRetrieval)
+
+    def getImageStorage(self):
+        site = grok.getSite()
+        if site is None:
+            return None
+        return site.get('images', None)
+
+    def isImageStorageEnabled(self):
+        site = grok.getSite()
+        if site is None:
+            return False
+        if site.get('images', None) is None:
+            return False
+        return True
+
+    def getFile(self, data):
+        # ImageStorage is disabled, so give fall-back behaviour for
+        # testing without ImageStorage
+        if not self.isImageStorageEnabled():
+            return StringIO(data)
+        storage = self.getImageStorage()
+        if storage is None:
+            raise ValueError('Cannot find an image storage')
+        result = storage.retrieveFile(data)
+        if result is None:
+            return StringIO(data)
+        return storage.retrieveFile(data)
+
+    def createFile(self, filename, f):
+        if not self.isImageStorageEnabled():
+            return WAeUPImageFile(filename, f.read())
+        storage = self.getImageStorage()
+        if storage is None:
+            raise ValueError('Cannot find an image storage')
+        file_id = storage.storeFile(f, filename)
+        return WAeUPImageFile(filename, file_id)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index.txt	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/index/README.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/README.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/README.txt	(revision 6703)
@@ -0,0 +1,206 @@
+Field Indexes
+=============
+
+Field indexes index orderable values.  Note that they don't check for
+orderability. That is, all of the values added to the index must be
+orderable together. It is up to applications to provide only mutually
+orderable values.
+
+    >>> from zope.index.field import FieldIndex
+
+    >>> index = FieldIndex()
+    >>> index.index_doc(0, 6)
+    >>> index.index_doc(1, 26)
+    >>> index.index_doc(2, 94)
+    >>> index.index_doc(3, 68)
+    >>> index.index_doc(4, 30)
+    >>> index.index_doc(5, 68)
+    >>> index.index_doc(6, 82)
+    >>> index.index_doc(7, 30)
+    >>> index.index_doc(8, 43)
+    >>> index.index_doc(9, 15)
+
+Field indexes are searched with apply.  The argument is a tuple
+with a minimum and maximum value:
+
+    >>> index.apply((30, 70))
+    IFSet([3, 4, 5, 7, 8])
+
+A common mistake is to pass a single value.  If anything other than a
+two-tuple is passed, a type error is raised:
+
+    >>> index.apply('hi')
+    Traceback (most recent call last):
+    ...
+    TypeError: ('two-length tuple expected', 'hi')
+
+
+Open-ended ranges can be provided by provinding None as an end point:
+
+    >>> index.apply((30, None))
+    IFSet([2, 3, 4, 5, 6, 7, 8])
+
+    >>> index.apply((None, 70))
+    IFSet([0, 1, 3, 4, 5, 7, 8, 9])
+
+    >>> index.apply((None, None))
+    IFSet([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
+
+To do an exact value search, supply equal minimum and maximum values:
+
+    >>> index.apply((30, 30))
+    IFSet([4, 7])
+
+    >>> index.apply((70, 70))
+    IFSet([])
+
+Field indexes support basic statistics:
+
+    >>> index.documentCount()
+    10
+    >>> index.wordCount()
+    8
+
+Documents can be reindexed:
+
+    >>> index.apply((15, 15))
+    IFSet([9])
+    >>> index.index_doc(9, 14)
+
+    >>> index.apply((15, 15))
+    IFSet([])
+    >>> index.apply((14, 14))
+    IFSet([9])
+
+Documents can be unindexed:
+
+    >>> index.unindex_doc(7)
+    >>> index.documentCount()
+    9
+    >>> index.wordCount()
+    8
+    >>> index.unindex_doc(8)
+    >>> index.documentCount()
+    8
+    >>> index.wordCount()
+    7
+
+    >>> index.apply((30, 70))
+    IFSet([3, 4, 5])
+
+Unindexing a document id that isn't present is ignored:
+
+    >>> index.unindex_doc(8)
+    >>> index.unindex_doc(80)
+    >>> index.documentCount()
+    8
+    >>> index.wordCount()
+    7
+
+We can also clear the index entirely:
+
+    >>> index.clear()
+    >>> index.documentCount()
+    0
+    >>> index.wordCount()
+    0
+
+    >>> index.apply((30, 70))
+    IFSet([])
+
+Sorting
+-------
+
+Field indexes also implement IIndexSort interface that
+provides a method for sorting document ids by their indexed
+values.
+
+    >>> index.index_doc(1, 9)
+    >>> index.index_doc(2, 8)
+    >>> index.index_doc(3, 7)
+    >>> index.index_doc(4, 6)
+    >>> index.index_doc(5, 5)
+    >>> index.index_doc(6, 4)
+    >>> index.index_doc(7, 3)
+    >>> index.index_doc(8, 2)
+    >>> index.index_doc(9, 1)
+
+    >>> list(index.sort([4, 2, 9, 7, 3, 1, 5]))
+    [9, 7, 5, 4, 3, 2, 1]
+
+We can also specify the ``reverse`` argument to reverse results:
+
+    >>> list(index.sort([4, 2, 9, 7, 3, 1, 5], reverse=True))
+    [1, 2, 3, 4, 5, 7, 9]
+
+And as per IIndexSort, we can limit results by specifying the ``limit``
+argument:
+
+    >>> list(index.sort([4, 2, 9, 7, 3, 1, 5], limit=3))
+    [9, 7, 5]
+
+If we pass an id that is not indexed by this index, it won't be included
+in the result.
+
+    >>> list(index.sort([2, 10]))
+    [2]
+
+    >>> index.clear()
+
+Bugfix testing:
+---------------
+Happened at least once that the value dropped out of the forward index,
+but the index still contains the object, the unindex broke
+
+    >>> index.index_doc(0, 6)
+    >>> index.index_doc(1, 26)
+    >>> index.index_doc(2, 94)
+    >>> index.index_doc(3, 68)
+    >>> index.index_doc(4, 30)
+    >>> index.index_doc(5, 68)
+    >>> index.index_doc(6, 82)
+    >>> index.index_doc(7, 30)
+    >>> index.index_doc(8, 43)
+    >>> index.index_doc(9, 15)
+
+    >>> index.apply((None, None))
+    IFSet([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
+
+Here is the damage:
+
+    >>> del index._fwd_index[68]
+
+Unindex should succeed:
+
+    >>> index.unindex_doc(5)
+    >>> index.unindex_doc(3)
+
+    >>> index.apply((None, None))
+    IFSet([0, 1, 2, 4, 6, 7, 8, 9])
+
+
+Optimizations
+-------------
+
+There is an optimization which makes sure that nothing is changed in the
+internal data structures if the value of the ducument was not changed.
+
+To test this optimization we patch the index instance to make sure unindex_doc
+is not called.
+
+    >>> def unindex_doc(doc_id):
+    ...     raise KeyError
+    >>> index.unindex_doc = unindex_doc
+
+Now we get a KeyError if we try to change the value.
+
+    >>> index.index_doc(9, 14)
+    Traceback (most recent call last):
+    ...
+    KeyError
+
+Leaving the value unchange doesn't call unindex_doc.
+
+    >>> index.index_doc(9, 15)
+    >>> index.apply((15, 15))
+    IFSet([9])
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/__init__.py	(revision 6703)
@@ -0,0 +1,1 @@
+from uniquefield import UniqueFieldIndex, UniqueField
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/configure.zcml
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/configure.zcml	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/configure.zcml	(revision 6703)
@@ -0,0 +1,10 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+  <class class=".uniquefield.UniqueFieldIndex">
+    <require
+        permission="zope.ManageServices"
+        interface="zope.catalog.interfaces.IAttributeIndex
+                   zope.index.interfaces.IStatistics"
+        set_schema="zope.catalog.interfaces.IAttributeIndex"
+        />
+  </class>
+</configure>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/interfaces.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/interfaces.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/interfaces.py	(revision 6703)
@@ -0,0 +1,54 @@
+##
+## interfaces.py
+## Login : <uli@pu.smp.net>
+## Started on  Sat May 28 13:36:51 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
+##
+"""
+Interfaces for :mod:`waeup.sirp.index`.
+"""
+from zope.index.interfaces import IInjection
+from zope.catalog.field import IFieldIndex
+
+class IUnique(IInjection):
+    def index_doc(docid, value):
+        """Add a document to the index.
+
+        `docid`: int, identifying the document
+
+        `value`: the value to be indexed
+
+        return: None
+
+        This can also be used to reindex documents.
+
+        If the same value is already stored under a different `docid`,
+        a :exc:`KeyError` is raised.
+        """
+
+
+class IUniqueFieldIndex(IFieldIndex):
+    """A field index that only accepts unique values.
+    """
+
+    def index_doc(docid, value):
+        """Index `value` under the given `docid`.
+
+        Raises :exc:`KeyError` if the value is already stored for a
+        different docid (unique-constraint violation).
+        """
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/tests.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/tests.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/tests.py	(revision 6703)
@@ -0,0 +1,387 @@
+"""Test unique field index
+"""
+import unittest
+from zope.catalog.catalog import Catalog
+from waeup.sirp.index.uniquefield import UniqueFieldIndex as FieldIndex
+from zope.catalog.tests import (
+    PlacelessSetup, IntIdsStub, stoopid, stoopidCallable,)
+from zope.component import provideUtility
+from zope.intid.interfaces import IIntIds
+
+_marker = object()
+
+class RawUniqueFieldIndexTests(unittest.TestCase):
+
+    def _getTargetClass(self):
+        from waeup.sirp.index.uniquefield import RawUniqueFieldIndex
+        return RawUniqueFieldIndex
+
+    def _makeOne(self, family=_marker):
+        if family is _marker:
+            return self._getTargetClass()()
+        return self._getTargetClass()(family)
+
+    def _populateIndex(self, index):
+        index.index_doc(5, 1) # docid, obj
+        index.index_doc(2, 2)
+        index.index_doc(1, 3)
+        index.index_doc(3, 4)
+        index.index_doc(4, 5)
+        index.index_doc(8, 6)
+        index.index_doc(9, 7)
+        index.index_doc(7, 8)
+        index.index_doc(6, 9)
+        index.index_doc(11, 10)
+        index.index_doc(10, 11)
+
+    def test_class_conforms_to_IInjection(self):
+        from zope.interface.verify import verifyClass
+        from zope.index.interfaces import IInjection
+        verifyClass(IInjection, self._getTargetClass())
+
+    def test_instance_conforms_to_IInjection(self):
+        from zope.interface.verify import verifyObject
+        from zope.index.interfaces import IInjection
+        verifyObject(IInjection, self._makeOne())
+
+    def test_class_conforms_to_IUniqueInjection(self):
+        from zope.interface.verify import verifyClass
+        from waeup.sirp.index.interfaces import IUnique
+        verifyClass(IUnique, self._getTargetClass())
+
+    def test_instance_conforms_to_IUniqueInjection(self):
+        from zope.interface.verify import verifyObject
+        from waeup.sirp.index.interfaces import IUnique
+        verifyObject(IUnique, self._makeOne())
+
+    def test_class_conforms_to_IIndexSearch(self):
+        from zope.interface.verify import verifyClass
+        from zope.index.interfaces import IIndexSearch
+        verifyClass(IIndexSearch, self._getTargetClass())
+
+    def test_instance_conforms_to_IIndexSearch(self):
+        from zope.interface.verify import verifyObject
+        from zope.index.interfaces import IIndexSearch
+        verifyObject(IIndexSearch, self._makeOne())
+
+    def test_class_conforms_to_IStatistics(self):
+        from zope.interface.verify import verifyClass
+        from zope.index.interfaces import IStatistics
+        verifyClass(IStatistics, self._getTargetClass())
+
+    def test_instance_conforms_to_IStatistics(self):
+        from zope.interface.verify import verifyObject
+        from zope.index.interfaces import IStatistics
+        verifyObject(IStatistics, self._makeOne())
+
+    def test_ctor_defaults(self):
+        import BTrees
+        index = self._makeOne()
+        self.failUnless(index.family is BTrees.family32)
+        self.assertEqual(index.documentCount(), 0)
+        self.assertEqual(index.wordCount(), 0)
+
+    def test_ctor_explicit_family(self):
+        import BTrees
+        index = self._makeOne(BTrees.family64)
+        self.failUnless(index.family is BTrees.family64)
+
+    def test_index_doc_new(self):
+        index = self._makeOne()
+        index.index_doc(1, 'value')
+        self.assertEqual(index.documentCount(), 1)
+        self.assertEqual(index.wordCount(), 1)
+        self.failUnless(1 in index._rev_index)
+        self.failUnless('value' in index._fwd_index)
+
+    def test_index_doc_existing_same_value(self):
+        index = self._makeOne()
+        index.index_doc(1, 'value')
+        index.index_doc(1, 'value')
+        self.assertEqual(index.documentCount(), 1)
+        self.assertEqual(index.wordCount(), 1)
+        self.failUnless(1 in index._rev_index)
+        self.failUnless('value' in index._fwd_index)
+        self.assertEqual(list(index._fwd_index['value']), [1])
+
+    def test_index_doc_existing_new_value(self):
+        index = self._makeOne()
+        index.index_doc(1, 'value')
+        index.index_doc(1, 'new_value')
+        self.assertEqual(index.documentCount(), 1)
+        self.assertEqual(index.wordCount(), 1)
+        self.failUnless(1 in index._rev_index)
+        self.failIf('value' in index._fwd_index)
+        self.failUnless('new_value' in index._fwd_index)
+        self.assertEqual(list(index._fwd_index['new_value']), [1])
+
+    def test_unindex_doc_nonesuch(self):
+        index = self._makeOne()
+        index.unindex_doc(1) # doesn't raise
+
+    def test_unindex_doc_no_residual_fwd_values(self):
+        index = self._makeOne()
+        index.index_doc(1, 'value')
+        index.unindex_doc(1) # doesn't raise
+        self.assertEqual(index.documentCount(), 0)
+        self.assertEqual(index.wordCount(), 0)
+        self.failIf(1 in index._rev_index)
+        self.failIf('value' in index._fwd_index)
+
+    def test_unindex_doc_w_residual_fwd_values(self):
+        index = self._makeOne()
+        index.index_doc(1, 'value')
+        self.assertRaises(
+            KeyError,
+            index.index_doc, 2, 'value')
+        index.unindex_doc(1) # doesn't raise
+        self.assertEqual(index.documentCount(), 0)
+        self.assertEqual(index.wordCount(), 0)
+        self.failIf(1 in index._rev_index)
+        self.failIf(2 in index._rev_index)
+        self.failIf('value' in index._fwd_index)
+        self.assertRaises(
+            KeyError,
+            index._fwd_index.__getitem__, 'value')
+        self.assertEqual(index._fwd_index.get('value'), None)
+
+    def test_apply_non_tuple_raises(self):
+        index = self._makeOne()
+        self.assertRaises(TypeError, index.apply, ['a', 'b'])
+
+    def test_apply_empty_tuple_raises(self):
+        index = self._makeOne()
+        self.assertRaises(TypeError, index.apply, ('a',))
+
+    def test_apply_one_tuple_raises(self):
+        index = self._makeOne()
+        self.assertRaises(TypeError, index.apply, ('a',))
+
+    def test_apply_three_tuple_raises(self):
+        index = self._makeOne()
+        self.assertRaises(TypeError, index.apply, ('a', 'b', 'c'))
+
+    def test_apply_two_tuple_miss(self):
+        index = self._makeOne()
+        self.assertEqual(list(index.apply(('a', 'b'))), [])
+
+    def test_apply_two_tuple_hit(self):
+        index = self._makeOne()
+        index.index_doc(1, 'albatross')
+        self.assertEqual(list(index.apply(('a', 'b'))), [1])
+
+    def test_sort_w_limit_lt_1(self):
+        index = self._makeOne()
+        self.assertRaises(ValueError,
+                          lambda: list(index.sort([1, 2, 3], limit=0)))
+
+    def test_sort_w_empty_index(self):
+        index = self._makeOne()
+        self.assertEqual(list(index.sort([1, 2, 3])), [])
+
+    def test_sort_w_empty_docids(self):
+        index = self._makeOne()
+        index.index_doc(1, 'albatross')
+        self.assertEqual(list(index.sort([])), [])
+
+    def test_sort_w_missing_docids(self):
+        index = self._makeOne()
+        index.index_doc(1, 'albatross')
+        self.assertEqual(list(index.sort([2, 3])), [])
+
+    def test_sort_force_nbest_w_missing_docids(self):
+        index = self._makeOne()
+        index._use_nbest = True
+        index.index_doc(1, 'albatross')
+        self.assertEqual(list(index.sort([2, 3])), [])
+
+    def test_sort_force_lazy_w_missing_docids(self):
+        index = self._makeOne()
+        index._use_lazy = True
+        index.index_doc(1, 'albatross')
+        self.assertEqual(list(index.sort([2, 3])), [])
+
+    def test_sort_lazy_nolimit(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        index._use_lazy = True
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1)
+        self.assertEqual(list(result), [5, 2, 1, 3, 4])
+
+    def test_sort_lazy_withlimit(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        index._use_lazy = True
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1, limit=3)
+        self.assertEqual(list(result), [5, 2, 1])
+
+    def test_sort_nonlazy_nolimit(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1)
+        self.assertEqual(list(result), [5, 2, 1, 3, 4])
+
+    def test_sort_nonlazy_missingdocid(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5, 99])
+        result = index.sort(c1)
+        self.assertEqual(list(result), [5, 2, 1, 3, 4]) # 99 not present
+
+    def test_sort_nonlazy_withlimit(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1, limit=3)
+        self.assertEqual(list(result), [5, 2, 1])
+
+    def test_sort_nonlazy_reverse_nolimit(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1, reverse=True)
+        self.assertEqual(list(result), [4, 3, 1, 2, 5])
+
+    def test_sort_nonlazy_reverse_withlimit(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1, reverse=True, limit=3)
+        self.assertEqual(list(result), [4, 3, 1])
+
+    def test_sort_nbest(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        index._use_nbest = True
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1, limit=3)
+        self.assertEqual(list(result), [5, 2, 1])
+
+    def test_sort_nbest_reverse(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        index._use_nbest = True
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1, reverse=True, limit=3)
+        self.assertEqual(list(result), [4, 3, 1])
+
+    def test_sort_nbest_missing(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        index._use_nbest = True
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5, 99])
+        result = index.sort(c1, limit=3)
+        self.assertEqual(list(result), [5, 2, 1])
+
+    def test_sort_nbest_missing_reverse(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        index._use_nbest = True
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5, 99])
+        result = index.sort(c1, reverse=True, limit=3)
+        self.assertEqual(list(result), [4, 3, 1])
+
+    def test_sort_nodocs(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1)
+        self.assertEqual(list(result), [])
+
+    def test_sort_nodocids(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        self._populateIndex(index)
+        c1 = IFSet()
+        result = index.sort(c1)
+        self.assertEqual(list(result), [])
+
+    def test_sort_badlimit(self):
+        from BTrees.IFBTree import IFSet
+        index = self._makeOne()
+        self._populateIndex(index)
+        c1 = IFSet([1, 2, 3, 4, 5])
+        result = index.sort(c1, limit=0)
+        self.assertRaises(ValueError, list, result)
+
+
+class TestCatalogBugs(PlacelessSetup, unittest.TestCase):
+    """I found that z.a.catalog, AttributeIndex failed to remove the previous
+    value/object from the index IF the NEW value is None.
+    """
+
+    def test_updateIndexWithNone(self):
+        uidutil = IntIdsStub()
+        provideUtility(uidutil, IIntIds)
+
+        catalog = Catalog()
+        index = FieldIndex('author', None)
+        catalog['author'] = index
+
+        ob1 = stoopid(author = "joe")
+        ob1id = uidutil.register(ob1)
+        catalog.index_doc(ob1id, ob1)
+
+        res = catalog.searchResults(author=('joe','joe'))
+        names = [x.author for x in res]
+        names.sort()
+        self.assertEqual(len(names), 1)
+        self.assertEqual(names, ['joe'])
+
+        ob1.author = None
+        catalog.index_doc(ob1id, ob1)
+
+        #the index must be empty now because None values are never indexed
+        res = catalog.searchResults(author=(None, None))
+        self.assertEqual(len(res), 0)
+
+    def test_updateIndexFromCallableWithNone(self):
+        uidutil = IntIdsStub()
+        provideUtility(uidutil, IIntIds)
+
+        catalog = Catalog()
+        index = FieldIndex('getAuthor', None, field_callable=True)
+        catalog['author'] = index
+
+        ob1 = stoopidCallable(author = "joe")
+
+        ob1id = uidutil.register(ob1)
+        catalog.index_doc(ob1id, ob1)
+
+        res = catalog.searchResults(author=('joe','joe'))
+        names = [x.author for x in res]
+        names.sort()
+        self.assertEqual(len(names), 1)
+        self.assertEqual(names, ['joe'])
+
+        ob1.author = None
+        catalog.index_doc(ob1id, ob1)
+
+        #the index must be empty now because None values are never indexed
+        res = catalog.searchResults(author=(None, None))
+        self.assertEqual(len(res), 0)
+
+def test_suite():
+    from zope.testing import doctest
+    return unittest.TestSuite((
+        doctest.DocFileSuite('README.txt', optionflags=doctest.ELLIPSIS),
+        unittest.makeSuite(RawUniqueFieldIndexTests),
+        unittest.makeSuite(TestCatalogBugs),
+        ))
+
+if __name__=='__main__':
+    unittest.main(defaultTest='test_suite')
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/uniquefield.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/uniquefield.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/index/uniquefield.py	(revision 6703)
@@ -0,0 +1,57 @@
+##
+## uniquefield.py
+## Login : <uli@pu.smp.net>
+## Started on  Sat May 28 13:39:15 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
+##
+"""
+Unique field indexes.
+"""
+from grok.index import IndexDefinition
+from zope.catalog.field import FieldIndex
+from zope.index.field import FieldIndex as RawFieldIndex
+from zope.interface import implements
+from waeup.sirp.index.interfaces import IUniqueFieldIndex, IUnique
+
+class RawUniqueFieldIndex(RawFieldIndex):
+    """Unique field index.
+    """
+    implements(IUnique)
+
+    def index_doc(self, docid, value):
+        """Index some doc.
+        """
+        entries = self._fwd_index.get(value, ())
+        if len(entries) > 0 and docid not in entries:
+            # Accept already existing entries if docid matches
+            # ('reindex' use case). If docid is not contained already,
+            # we have a unique-constraint violation.
+            raise KeyError(
+                'The value already exists in catalog: %s' % value,)
+        return super(RawUniqueFieldIndex, self).index_doc(docid, value)
+
+class UniqueFieldIndex(FieldIndex):
+    implements(IUniqueFieldIndex)
+
+class UniqueField(IndexDefinition):
+    """A :class:`grok.Indexes` index that matches against an entire field.
+
+    Contrary to :class:`grok.index.Field` this index only accepts
+    unique values.
+    """
+    index_class = UniqueFieldIndex
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/interfaces.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/interfaces.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/interfaces.py	(revision 6703)
@@ -0,0 +1,485 @@
+##
+## interfaces.py
+import os
+from hurry.workflow.interfaces import IWorkflow, IWorkflowInfo
+from zc.sourcefactory.basic import BasicSourceFactory
+from zope import schema
+from zope.component import getUtility
+from zope.component.interfaces import IObjectEvent
+from zope.interface import Interface, Attribute, implements
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+default_frontpage = u'' + open(os.path.join(
+        os.path.dirname(__file__), 'frontpage.rst'), 'rb').read()
+
+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
+
+def SimpleWAeUPVocabulary(*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])
+
+class RoleSource(BasicSourceFactory):
+    """A source for global roles.
+    """
+    def getValues(self):
+        # late import: in interfaces we should not import local modules
+        from waeup.sirp.permissions import getWAeUPRoleNames
+        return getWAeUPRoleNames()
+
+    def getTitle(self, value):
+        # late import: in interfaces we should not import local modules
+        from waeup.sirp.permissions import getRoles
+        roles = dict(getRoles())
+        if value in roles.keys():
+            title = roles[value].title
+            if '.' in title:
+                title = title.split('.', 2)[1]
+        return title
+
+class IWAeUPObject(Interface):
+    """A WAeUP object.
+
+    This is merely a marker interface.
+    """
+
+class IUniversity(IWAeUPObject):
+    """Representation of a university.
+    """
+    name = schema.TextLine(
+        title = u'Name of University',
+        default = u'Sample University',
+        required = True,
+        )
+
+    title = schema.TextLine(
+        title = u'Title of frontpage',
+        default = u'Welcome to the Student Information and Registration ' +
+                  u'Portal of Sample University',
+        required = False,
+        )
+
+    skin = schema.Choice(
+        title = u'Skin',
+        default = u'waeuptheme-gray1.css',
+        vocabulary = 'waeup.sirp.browser.theming.ThemesVocabulary',
+        required = True,
+        )
+
+    frontpage = schema.Text(
+        title = u'Content in reST format',
+        required = False,
+        default = default_frontpage,
+        )
+
+
+class IWAeUPContainer(IWAeUPObject):
+    """A container for WAeUP objects.
+    """
+
+class IWAeUPContained(IWAeUPObject):
+    """An item contained in an IWAeUPContainer.
+    """
+
+class IStudentContainer(IWAeUPContainer):
+    """A container for StudentObjects.
+    """
+
+
+class IHostelContainer(IWAeUPContainer):
+    """A container for hostels.
+    """
+    def addHostel(hostel):
+        """Add an IHostel object.
+
+        Returns the key, under which the object was stored.
+        """
+
+class IHostel(IWAeUPObject):
+    """Representation of a hostel.
+    """
+    name = schema.TextLine(
+        title = u'Name of Hostel',
+        default = u'Nobody',
+        required = True,
+        )
+
+
+class IWAeUPExporter(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 IWAeUPXMLExporter(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 IWAeUPXMLImporter(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 IUserAccount(IWAeUPObject):
+    """A user account.
+    """
+    name = schema.TextLine(
+        title = u'User ID',
+        description = u'Login name of user',
+        required = True,)
+    title = schema.TextLine(
+        title = u'Name',
+        description = u'Real name of user',
+        required = False,)
+    description = schema.TextLine(
+        title = u'Description',
+        required = False,)
+    password = schema.Password(
+        title = u'Password',
+        required = True,)
+    roles = schema.List(
+        title = u'Global roles',
+        value_type = schema.Choice(source=RoleSource()))
+
+
+class IUserContainer(IWAeUPObject):
+    """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 IDataCenter(IWAeUPObject):
+    """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 IWAeUPSIRPPluggable(Interface):
+    """A component that might be plugged into a WAeUP 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 IWAeUPWorkflowInfo(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 WAeUPLoggers")
+
+    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 WAeUP 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.
+        """
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/meta.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/meta.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/meta.py	(revision 6703)
@@ -0,0 +1,111 @@
+##
+## meta.py
+## Login : <uli@pu.smp.net>
+## Started on  Wed May 18 23:38:13 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
+##
+"""Grokkers for WAeUP 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 = WAeUPIndexesUpgradeSubscriber(
+            catalog_name, indexes, context, module_info)
+        subscribed = (site, IObjectUpgradeEvent)
+        config.action(
+            discriminator=None,
+            callable=component.provideHandler,
+            args=(subscriber, subscribed),
+            )
+        return True
+
+
+class WAeUPIndexesUpgradeSubscriber(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/uli-studentpw/src/waeup/sirp/meta.zcml
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/meta.zcml	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/meta.zcml	(revision 6703)
@@ -0,0 +1,12 @@
+<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" />
+
+</configure>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/objecthistory.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/objecthistory.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/objecthistory.py	(revision 6703)
@@ -0,0 +1,73 @@
+##
+## objecthistory.py
+## Login : <uli@pu.smp.net>
+## Started on  Fri Jun 10 16:06:02 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
+##
+import grok
+from datetime import datetime
+from persistent.list import PersistentList
+from zope.annotation.interfaces import IAnnotations
+from waeup.sirp.interfaces import IObjectHistory, IWAeUPObject
+from waeup.sirp.utils.helpers import get_current_principal
+
+class ObjectHistory(grok.Adapter):
+    """A history for objects.
+
+    For any object implementing `IWAeUPObject` which is annotatable,
+    we provide histories. A history for such an object can be obtained
+    by adapting it to `IObjectHistory`.
+    """
+    grok.context(IWAeUPObject)
+    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.title == 'Applicant':
+            user = 'applicant'
+        else:
+            user = user.id
+        msg = '%s - %s by %s' % (timestamp, msg, user)
+        msgs.append(msg)
+        self._annotations[self.history_key] = msgs
+        return
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/permissions.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/permissions.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/permissions.py	(revision 6703)
@@ -0,0 +1,172 @@
+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 ViewPermission(grok.Permission):
+    grok.name('waeup.View')
+
+class ManageUniversity(grok.Permission):
+    grok.name('waeup.manageUniversity')
+
+class ManageUsers(grok.Permission):
+    grok.name('waeup.manageUsers')
+
+class ManageDataCenter(grok.Permission):
+    grok.name('waeup.manageDataCenter')
+
+class ManagePortalSettings(grok.Permission):
+    grok.name('waeup.managePortalSettings')
+
+# Local Roles
+class DepartmentOfficer(grok.Role):
+    grok.name('waeup.local.DepartmentOfficer')
+    grok.title(u'Department Officer')
+    grok.permissions('waeup.manageUniversity','waeup.View', 'waeup.Public')
+
+class ClearanceOfficer(grok.Role):
+    grok.name('waeup.local.ClearanceOfficer')
+    grok.title(u'Clearance Officer')
+    # to be further defined
+    grok.permissions('waeup.View', 'waeup.Public')
+
+class CourseAdviser(grok.Role):
+    grok.name('waeup.local.CourseAdviser')
+    grok.title(u'Course Adviser')
+    # to be further defined
+    grok.permissions('waeup.View', 'waeup.Public')
+
+# Global Roles
+class PortalUser(grok.Role):
+    grok.name('waeup.PortalUser')
+    grok.title(u'Portal User')
+    grok.permissions('waeup.View', 'waeup.Public')
+
+class PortalManager(grok.Role):
+    grok.name('waeup.PortalManager')
+    grok.title(u'Portal Manager')
+    grok.permissions('waeup.manageUniversity', 'waeup.manageUsers',
+                     'waeup.View', 'waeup.Public','waeup.manageACBatches',
+                     'waeup.manageDataCenter','waeup.managePortalSettings',
+                     'waeup.manageApplications', 'waeup.handleApplication',
+                     'waeup.viewStudents', 'waeup.manageStudents')
+
+def getRoles():
+    """Return a list of tuples ``<ROLE-NAME>, <ROLE>``.
+    """
+    return getUtilitiesFor(IRole)
+
+def getWAeUPRoles(also_local=False):
+    """Get all WAeUP roles.
+
+    WAeUP 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 WAeUP 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 getRoles():
+        if not name.startswith('waeup.'):
+            # Ignore non-WAeUP roles...
+            continue
+        if not also_local and name.startswith('waeup.local.'):
+            # Ignore local roles...
+            continue
+        yield item
+
+def getWAeUPRoleNames():
+    """Get the ids of all WAeUP roles.
+
+    See :func:`getWAeUPRoles` for what a 'WAeUPRole' is.
+
+    This function returns a sorted list of WAeUP role names.
+    """
+    return sorted([x.id for x in getWAeUPRoles()])
+
+
+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 getRoles()
+                       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).
+        """
+        return [
+            dict(
+                name=name,
+                title=role.title,
+                description=role.description)
+            for name, role in self._roles]
+
+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, 'description', user_name)
+        local_role_title = dict(getRoles())[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/uli-studentpw/src/waeup/sirp/permissions.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/permissions.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/permissions.txt	(revision 6703)
@@ -0,0 +1,75 @@
+WAeUP permissions and roles
+***************************
+
+Permissions and roles used in a WAeUP portal.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
+
+Convenience functions
+=====================
+
+:mod:`waeup.sirp` offers some convenience functions to handle security
+roles.
+
+:func:`getRoles`
+----------------
+
+Gives us all roles defined in a WAeUP SIRP portal. 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 getRoles
+    >>> getRoles()
+    <generator object...at 0x...>
+
+    >>> sorted(list(getRoles()))
+    [(u'waeup.ApplicationsOfficer', <waeup.sirp.applicants.permissions.ApplicationsOfficer object at 0x...>), ...]
+
+:func:`getWAeUPRoles`
+---------------------
+
+Gives us all roles, except the WAeUP specific roles. We can get a list
+with or without local roles:
+
+    >>> from waeup.sirp.permissions import getWAeUPRoles
+    >>> len(list(getWAeUPRoles()))
+    5
+
+    >>> len(list(getWAeUPRoles(also_local=True)))
+    10
+
+
+:func:`getRoleNames`
+--------------------
+
+We can get all role names defined in a WAeUP portal (except 'local'
+roles that are meant not to be assigned globally):
+
+    >>> from waeup.sirp.permissions import getWAeUPRoleNames
+    >>> list(getWAeUPRoleNames())
+    [u'waeup.ApplicationsOfficer', u'waeup.PortalManager',
+     u'waeup.PortalUser', u'waeup.Student', 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/uli-studentpw/src/waeup/sirp/students/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/__init__.py	(revision 6703)
@@ -0,0 +1,20 @@
+"""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 StudentPayments
+from waeup.sirp.students.accommodation import StudentAccommodation
+
+
+__all__ = [
+    'Student',
+    'StudentFactory',
+    'StudentsContainer',
+    'StudentStudyCourse',
+    'StudentPayments',
+    'StudentAccommodation'
+    ]
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/accommodation.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/accommodation.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/accommodation.py	(revision 6703)
@@ -0,0 +1,39 @@
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 contains the (student) accommodation objects.
+"""
+import grok
+from grok import index
+from zope.component.interfaces import IFactory
+from waeup.sirp.students.interfaces import (
+    IStudentAccommodation, IStudentNavigation)
+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 getStudent(self):
+        return self.__parent__
+
+StudentAccommodation = attrs_to_fields(StudentAccommodation)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/authentication.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/authentication.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/authentication.py	(revision 6703)
@@ -0,0 +1,176 @@
+##
+## authentication.py
+## Login : <uli@pu.smp.net>
+## Started on  Fri Sep  2 15:22:47 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
+##
+"""
+Authenticate students.
+"""
+import grok
+from zope.component import getUtility
+from zope.password.interfaces import IPasswordManager
+from zope.pluggableauth.interfaces import IAuthenticatorPlugin
+from waeup.sirp.authentication import PrincipalInfo, get_principal_role_manager
+from waeup.sirp.interfaces import IAuthPluginUtility, IUserAccount
+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.name
+
+    @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
+        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 PrincipalInfo(id=account.name,
+                             title=account.title,
+                             description=account.description)
+
+    def principalInfo(self, id):
+        """Get a principal identified by `id`.
+
+        This one is required by IAuthenticatorPlugin.
+        """
+        account = self.getAccount(id)
+        if account is None:
+            return None
+        return PrincipalInfo(id=account.name,
+                             title=account.title,
+                             description=account.description)
+
+    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 StudentsAuthenticatorSetup(grok.GlobalUtility):
+    """Register or unregister student authentication for a PAU.
+
+    This piece is called when a new site is created.
+    """
+    grok.implements(IAuthPluginUtility)
+
+    def register(self, pau):
+        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/uli-studentpw/src/waeup/sirp/students/browser.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser.py	(revision 6703)
@@ -0,0 +1,592 @@
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 datetime import datetime
+from zope.formlib.widget import CustomWidgetFactory
+from zope.formlib.form import setUpEditWidgets
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.traversing.browser import absoluteURL
+from zope.component import (
+    createObject,)
+
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+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 waeup.sirp.accesscodes import invalidate_accesscode, get_access_code
+from waeup.sirp.accesscodes.workflow import USED
+from waeup.sirp.browser import (
+    WAeUPPage, WAeUPEditFormPage, WAeUPAddFormPage, WAeUPDisplayFormPage)
+from waeup.sirp.browser.breadcrumbs import Breadcrumb
+from waeup.sirp.browser.layout import NullValidator
+from waeup.sirp.browser.pages import add_local_role, del_local_roles
+from waeup.sirp.browser.resources import datepicker, tabs, datatable
+from waeup.sirp.browser.viewlets import (
+    ManageActionButton, PrimaryNavTab,
+    AddActionButton, ActionButton, PlainActionButton,
+    )
+from waeup.sirp.image.browser.widget import (
+    ThumbnailWidget, EncodingImageFileWidget,
+    )
+from waeup.sirp.image.image import createWAeUPImageFile
+from waeup.sirp.interfaces import (
+    IWAeUPObject, ILocalRolesAssignable, IUserAccount,
+    )
+from waeup.sirp.permissions import get_users_with_local_roles
+from waeup.sirp.university.interfaces import ICertificate
+from waeup.sirp.widgets.datewidget import (
+    FriendlyDateWidget, FriendlyDateDisplayWidget)
+from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
+from waeup.sirp.widgets.objectwidget import (
+    WAeUPObjectWidget, WAeUPObjectDisplayWidget)
+from waeup.sirp.students.interfaces import (
+    IStudentsContainer, IStudent, IStudentClearance,
+    IStudentPersonal, IStudentBase, IStudentStudyCourse,
+    IStudentPayments, IStudentAccommodation, IStudentNavigation,
+    IStudentBaseEdit, IStudentClearanceEdit,
+    )
+from waeup.sirp.students.student import Student
+from waeup.sirp.students.catalog import search
+from waeup.sirp.accesscodes import invalidate_accesscode, get_access_code
+from waeup.sirp.accesscodes.workflow import USED
+
+class StudentsTab(PrimaryNavTab):
+    """Students tab in primary navigation.
+    """
+
+    grok.context(IWAeUPObject)
+    grok.order(3)
+    grok.require('waeup.viewStudent')
+    grok.template('primarynavtab')
+
+    pnav = 4
+    tab_title = u'Students'
+
+    @property
+    def link_target(self):
+        return self.view.application_url('students')
+
+class StudentsBreadcrumb(Breadcrumb):
+    """A breadcrumb for the students container.
+    """
+    grok.context(IStudentsContainer)
+    title = u'Students'
+
+class SudyCourseBreadcrumb(Breadcrumb):
+    """A breadcrumb for the student study course.
+    """
+    grok.context(IStudentStudyCourse)
+    title = u'Study Course'
+
+class PaymentsBreadcrumb(Breadcrumb):
+    """A breadcrumb for the student payments folder.
+    """
+    grok.context(IStudentPayments)
+    title = u'Payments'
+
+class AccommodationBreadcrumb(Breadcrumb):
+    """A breadcrumb for the student accommodation folder.
+    """
+    grok.context(IStudentAccommodation)
+    title = u'Accommodation'
+
+class StudentsContainerPage(WAeUPPage):
+    """The standard view for student containers.
+    """
+    grok.context(IStudentsContainer)
+    grok.name('index')
+    grok.require('waeup.viewStudent')
+    grok.template('containerpage')
+    label = 'Student Section'
+    title = 'Students'
+    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
+        self.hitlist = search(query=self.searchterm,
+            searchtype=self.searchtype, view=self)
+        if not self.hitlist:
+            self.flash('No student found.')
+        return
+
+class SetPassword(WAeUPPage):
+    grok.context(IWAeUPObject)
+    grok.name('setpassword')
+    grok.require('waeup.Public')
+    title = ''
+    label = 'Set password for first-time login'
+    acprefix = 'PWD'
+    pnav = 4
+
+    def update(self, SUBMIT=None):
+        self.reg_number = self.request.form.get('form.reg_number', None)
+        # We must not use form.ac_series and form.ac_number in forms since these
+        # are interpreted by applicant credentials
+        self.acseries = self.request.form.get('form.acseries', None)
+        self.acnumber = self.request.form.get('form.acnumber', 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
+        self.student_id = hitlist[0].student_id
+        student_pw = hitlist[0].context.password
+        if student_pw:
+            self.flash('Password has already been set. Please request a password reset.')
+            return
+        pin = '%s-%s-%s' % (self.acprefix,self.acseries,self.acnumber)
+        code = get_access_code(pin)
+        if not code:
+            self.flash('Access code is invalid.')
+            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"AC invalidated"
+            # Here we know that the ac is in state initialized so we do not
+            # expect an exception
+            #import pdb; pdb.set_trace()
+            invalidate_accesscode(pin,comment)
+            IUserAccount(hitlist[0].context).setPassword(self.acnumber)
+        return
+
+class StudentsContainerManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudentsContainer)
+    grok.view(StudentsContainerPage)
+    grok.require('waeup.manageStudents')
+    text = 'Manage student section'
+
+
+class StudentsContainerManagePage(WAeUPPage):
+    """The manage page for student containers.
+    """
+    grok.context(IStudentsContainer)
+    grok.name('manage')
+    grok.require('waeup.manageStudents')
+    grok.template('containermanagepage')
+    pnav = 4
+    title = 'Manage student section'
+
+    @property
+    def label(self):
+        return self.title
+
+    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 not 'entries' in form:
+            self.hitlist = search(query=self.searchterm,
+                searchtype=self.searchtype, view=self)
+            if not self.hitlist:
+                self.flash('No student found.')
+            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: %s' % ', '.join(deleted))
+        return
+
+class StudentsContainerAddActionButton(AddActionButton):
+    grok.order(1)
+    grok.context(IStudentsContainer)
+    grok.view(StudentsContainerManagePage)
+    grok.require('waeup.manageStudents')
+    text = 'Add student'
+    target = 'addstudent'
+
+class StudentAddFormPage(WAeUPAddFormPage):
+    """Add-form to add a student.
+    """
+    grok.context(IStudentsContainer)
+    grok.require('waeup.manageStudents')
+    grok.name('addstudent')
+    grok.template('studentaddpage')
+    form_fields = grok.AutoFields(IStudent)
+    title = 'Students'
+    label = 'Add student'
+    pnav = 4
+
+    @grok.action('Create student record')
+    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(WAeUPDisplayFormPage):
+    """ 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
+    title = 'Base Data'
+
+    @property
+    def label(self):
+        return '%s: Base Data' % self.context.name
+
+    @property
+    def hasPassword(self):
+        if self.context.password:
+            return 'set'
+        return 'unset'
+
+class StudentBaseManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.manageStudents')
+    text = 'Manage'
+    target = 'edit_base'
+
+class StudentBaseManageFormPage(WAeUPEditFormPage):
+    """ View to edit student base data
+    """
+    grok.context(IStudent)
+    grok.name('edit_base')
+    grok.require('waeup.manageStudents')
+    form_fields = grok.AutoFields(IStudentBase).omit('student_id')
+    grok.template('basemanagepage')
+    label = 'Manage base data'
+    title = 'Base Data'
+    pnav = 4
+
+    def update(self):
+        datepicker.need() # Enable jQuery datepicker in date fields.
+        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]
+
+    @grok.action('Save')
+    def save(self, **data):
+        form = self.request.form
+        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
+        if form.has_key('password') and form['password']:
+            if form['password'] != form['control_password']:
+                self.flash('Passwords do not match.')
+                return
+            IUserAccount(self.context).setPassword(form['password'])
+            self.context.loggerInfo(ob_class, 'password changed')
+        changed_fields = self.applyData(self.context, **data)
+        changed_fields = changed_fields.values()
+        fields_string = '+'.join(' + '.join(str(i) for i in b) for b in changed_fields)
+        self.context._p_changed = True
+        if form.has_key('transition') and form['transition']:
+            transition_id = form['transition']
+            self.wf_info.fireTransition(transition_id)
+        self.flash('Form has been saved.')
+        if fields_string:
+            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
+        return
+
+class StudentClearanceDisplayFormPage(WAeUPDisplayFormPage):
+    """ 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')
+    title = 'Clearance Data'
+    pnav = 4
+
+    @property
+    def label(self):
+        return '%s: Clearance Data' % self.context.name
+
+class StudentClearanceManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentClearanceDisplayFormPage)
+    grok.require('waeup.manageStudents')
+    text = 'Manage'
+    target = 'edit_clearance'
+
+class StudentClearanceManageFormPage(WAeUPEditFormPage):
+    """ Page to edit student clearance data
+    """
+    grok.context(IStudent)
+    grok.name('edit_clearance')
+    grok.require('waeup.manageStudents')
+    form_fields = grok.AutoFields(IStudentClearance)
+    label = 'Manage clearance data'
+    title = '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.
+        return super(StudentClearanceManageFormPage, self).update()
+
+    @grok.action('Save')
+    def save(self, **data):
+        changed_fields = self.applyData(self.context, **data)
+        changed_fields = changed_fields.values()
+        fields_string = '+'.join(' + '.join(str(i) for i in b) for b in changed_fields)
+        self.context._p_changed = True
+        form = self.request.form
+        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
+
+class StudentPersonalDisplayFormPage(WAeUPDisplayFormPage):
+    """ Page to display student personal data
+    """
+    grok.context(IStudent)
+    grok.name('view_personal')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentPersonal)
+    title = 'Personal Data'
+    pnav = 4
+
+    @property
+    def label(self):
+        return '%s: Personal Data' % self.context.name
+
+class StudentPersonalManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentPersonalDisplayFormPage)
+    grok.require('waeup.manageStudents')
+    text = 'Manage'
+    target = 'edit_personal'
+
+class StudentPersonalManageFormPage(WAeUPEditFormPage):
+    """ 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'
+    title = 'Personal Data'
+    pnav = 4
+
+class StudyCourseDisplayFormPage(WAeUPDisplayFormPage):
+    """ 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')
+    title = 'Study Course'
+    pnav = 4
+
+    @property
+    def label(self):
+        return '%s: Study Course' % self.context.__parent__.name
+
+class StudyCourseManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudentStudyCourse)
+    grok.view(StudyCourseDisplayFormPage)
+    grok.require('waeup.manageStudents')
+    text = 'Manage'
+    target = 'edit'
+
+class StudyCourseManageFormPage(WAeUPEditFormPage):
+    """ Page to edit the student study course data
+    """
+    grok.context(IStudentStudyCourse)
+    grok.name('edit')
+    grok.require('waeup.manageStudents')
+    form_fields = grok.AutoFields(IStudentStudyCourse)
+    title = 'Study Course'
+    label = 'Manage study course'
+    pnav = 4
+
+class PaymentsDisplayFormPage(WAeUPDisplayFormPage):
+    """ Page to display the student payments
+    """
+    grok.context(IStudentPayments)
+    grok.name('index')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentPayments)
+    #grok.template('paymentspage')
+    title = 'Payments'
+    pnav = 4
+
+    @property
+    def label(self):
+        return '%s: Payments' % self.context.__parent__.name
+
+class AccommodationDisplayFormPage(WAeUPDisplayFormPage):
+    """ Page to display the student accommodation data
+    """
+    grok.context(IStudentAccommodation)
+    grok.name('index')
+    grok.require('waeup.viewStudent')
+    form_fields = grok.AutoFields(IStudentAccommodation)
+    #grok.template('accommodationpage')
+    title = 'Accommodation'
+    pnav = 4
+
+    @property
+    def label(self):
+        return '%s: Accommodation Data' % self.context.__parent__.name
+
+class StudentHistoryPage(WAeUPPage):
+    """ Page to display student clearance data
+    """
+    grok.context(IStudent)
+    grok.name('history')
+    grok.require('waeup.viewStudent')
+    grok.template('studenthistory')
+    title = 'History'
+    pnav = 4
+
+    @property
+    def label(self):
+        return '%s: History' % self.context.name
+
+# Pages for students only
+
+class StudentBaseEditActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.handleStudent')
+    text = 'Change password'
+    target = 'bedit'
+
+class StudentBaseEditFormPage(WAeUPEditFormPage):
+    """ View to edit student base data by student
+    """
+    grok.context(IStudent)
+    grok.name('bedit')
+    grok.require('waeup.handleStudent')
+    form_fields = grok.AutoFields(IStudentBaseEdit).omit(
+        'student_id', 'reg_number')
+    grok.template('baseeditpage')
+    label = 'Change password'
+    title = 'Base Data'
+    pnav = 4
+
+    def update(self):
+        datepicker.need() # Enable jQuery datepicker in date fields.
+        super(StudentBaseEditFormPage, self).update()
+        self.wf_info = IWorkflowInfo(self.context)
+        return
+
+    @grok.action('Save')
+    def save(self, **data):
+        form = self.request.form
+        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
+        if form.has_key('password') and form['password']:
+            if form['password'] != form['control_password']:
+                self.flash('Passwords do not match.')
+                return
+            IUserAccount(self.context).setPassword(form['password'])
+            self.context.loggerInfo(ob_class, 'password changed')
+        changed_fields = self.applyData(self.context, **data)
+        changed_fields = changed_fields.values()
+        fields_string = '+'.join(' + '.join(str(i) for i in b) for b in changed_fields)
+        self.context._p_changed = True
+        self.flash('Form has been saved.')
+        if fields_string:
+            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
+        return
+
+class StudentClearanceEditActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentClearanceDisplayFormPage)
+    grok.require('waeup.handleStudent')
+    text = 'Edit and submit'
+    target = 'cedit'
+
+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(IStudentClearanceEdit).omit('clearance_locked')
+    #grok.template('clearanceeditpage')
+    label = 'Edit clerance data'
+    title = 'Clearance Data'
+    pnav = 4
+    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/baseeditpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/baseeditpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/baseeditpage.pt	(revision 6703)
@@ -0,0 +1,60 @@
+<h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+<form action="." tal:attributes="action request/URL" method="post"
+      class="edit-form" enctype="multipart/form-data">
+
+  <div class="form-status"
+    tal:define="status view/status"
+    tal:condition="status">
+    Form Status:
+    <span i18n:translate="" tal:content="view/status">
+      Form status summary
+    </span>
+  </div>
+
+  <table class="form-fields zebra">
+    <tbody>
+      <tal:block repeat="widget view/widgets">
+        <tr>
+          <td class="label">
+            <label tal:attributes="for widget/name">
+              <span class="required" tal:condition="widget/required">*</span>
+              <span i18n:translate="" tal:content="widget/label">label</span>:
+            </label>
+          </td>
+          <td class="field">
+            <span class="widget" 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">
+              <span class="hint" tal:content="structure widget/hint">hint</span>
+            </tal:hint>
+          </td>
+        </tr>
+      </tal:block>
+      <tr>
+        <td class="label"><label>Password:</label></td>
+        <td>
+          <input name="password" type="password"/>
+        </td>
+      </tr>
+      <tr>
+        <td class="label"><label>Retype password:</label></td>
+        <td>
+          <input name="control_password" type="password" />
+        </td>
+      </tr>
+    </tbody>
+  </table>
+
+  <div class="actionButtons" tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/basemanagepage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/basemanagepage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/basemanagepage.pt	(revision 6703)
@@ -0,0 +1,71 @@
+<h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+<form action="." tal:attributes="action request/URL" method="post"
+      class="edit-form" enctype="multipart/form-data">
+
+  <div class="form-status"
+    tal:define="status view/status"
+    tal:condition="status">
+    Form Status:
+    <span i18n:translate="" tal:content="view/status">
+      Form status summary
+    </span>
+  </div>
+
+  <table class="form-fields zebra">
+    <tbody>
+      <tal:block repeat="widget view/widgets">
+        <tr>
+          <td class="label">
+            <label tal:attributes="for widget/name">
+              <span class="required" tal:condition="widget/required">*</span>
+              <span i18n:translate="" tal:content="widget/label">label</span>:
+            </label>
+          </td>
+          <td class="field">
+            <span class="widget" 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">
+              <span class="hint" tal:content="structure widget/hint">hint</span>
+            </tal:hint>
+          </td>
+        </tr>
+      </tal:block>
+      <tr>
+        <td class="label"><label>Password:</label></td>
+        <td>
+          <input name="password" type="password"  />
+        </td>
+      </tr>
+      <tr>
+        <td class="label"><label>Retype password:</label></td>
+        <td>
+          <input name="control_password" type="password" />
+        </td>
+      </tr>
+      <tr>
+        <td class="label"><label>Registration Transition:</label></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 class="actionButtons" tal:condition="view/availableActions">
+    <input tal:repeat="action view/actions"
+           tal:replace="structure action/render"
+           />
+  </div>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/basepage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/basepage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/basepage.pt	(revision 6703)
@@ -0,0 +1,34 @@
+<h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+<table class="zebra">
+  <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">
+          Password:
+      </td>
+      <td>
+          <tal:password replace="view/hasPassword" />
+      </td>
+    <tr>
+  </tbody>
+  <tfoot>
+    <tr class="controls">
+      <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/uli-studentpw/src/waeup/sirp/students/browser_templates/containermanagepage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/containermanagepage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/containermanagepage.pt	(revision 6703)
@@ -0,0 +1,46 @@
+<h2 tal:content="view/label">Title</h2>
+<form method="POST">
+
+<br />
+
+  <input type="submit" name="search" value="Search" />
+  for students
+
+  <select name="searchtype">
+    <option value="student_id">with id</option>
+    <option value="name">with name</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>Student Id</th><th>Name</th>
+    	</tr>
+      </thead>
+      <tbody>
+    	<tr tal:repeat="item view/hitlist">
+    	  <td><input type="checkbox" name="entries"
+    	             tal:attributes="value item/context/__name__" /></td>
+    	  <td> <a tal:attributes="href item/url">
+          <span tal:content="item/student_id">A123456</span></a>
+        </td>
+    	  <td tal:content="item/name">Bob</td>
+    	</tr>
+      </tbody>
+    </table>
+    <div class="actionButtons">
+        <input type="submit" name="remove" value="Remove selected" />
+    </div>
+
+  </div>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/containerpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/containerpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/containerpage.pt	(revision 6703)
@@ -0,0 +1,40 @@
+<h2 tal:content="view/label">Title</h2>
+<form method="POST">
+
+<br />
+
+  <input type="submit" name="search" value="Search" />
+  for students
+
+  <select name="searchtype">
+    <option value="student_id">with id</option>
+    <option value="name">with name</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>Student Id</th><th>Name</th>
+    	</tr>
+      </thead>
+      <tbody>
+    	<tr tal:repeat="item view/hitlist">
+    	  <td> <a tal:attributes="href item/url">
+          <span tal:content="item/student_id">A123456</span></a>
+        </td>
+    	  <td tal:content="item/name">Bob</td>
+    	</tr>
+      </tbody>
+    </table>
+    <br />
+  </div>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/plainactionbutton.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/plainactionbutton.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/plainactionbutton.pt	(revision 6703)
@@ -0,0 +1,7 @@
+<span class="yui-button">
+  <span class="first-child">
+    <a href=""
+       tal:attributes="href viewlet/target_url">
+    <span tal:content="viewlet/text">TEXT</span></a>
+  </span>
+</span>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/primarynavtab.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/primarynavtab.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/primarynavtab.pt	(revision 6703)
@@ -0,0 +1,7 @@
+<li tal:attributes="class viewlet/active">
+  <a href="#"
+     tal:attributes="href viewlet/link_target"
+     tal:content="viewlet/tab_title">
+    Primary Tab Title
+  </a>
+</li>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/setpassword.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/setpassword.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/setpassword.pt	(revision 6703)
@@ -0,0 +1,39 @@
+<h2 i18n:translate=""  tal:condition="view/label"  tal:content="view/label">Label</h2>
+<form method="POST">
+  <input type="hidden" name="form.acprefix"  value="" tal:attributes="value view/acprefix" />
+  <table>
+    <tbody>
+      <tr> <td>
+          <label for="form.reg_number">Registration Number:
+          </label></td> <td>
+          <input name="form.reg_number" type="text" size="15" maxlength="15"  value="" tal:attributes="value view/reg_number"/> </td>
+      </tr>
+      <tr>
+        <tr> <td>
+            <label for="form.acseries">Access Code:
+            </label></td> <td>
+            <span tal:replace="view/acprefix">PIN Prefix
+            </span> -
+            <input name="form.acseries" type="text" size="3" maxlength="3"  value="" tal:attributes="value view/acseries"/> -
+            <input name="form.acnumber" type="text" size="10" maxlength="10"  value="" tal:attributes="value view/acnumber" /> </td>
+        </tr>
+        <tr>
+          <td colspan="2">
+            <input type="submit" name="SUBMIT" value="Show Student Id and set Password" /> </td>
+        </tr>
+    </tbody>
+  </table>
+</form>
+<div> <h3>Instructions:</h3>
+  <ul class="list">
+    <li> Purchase a Password Access Code (PWD PIN).
+    </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 new password is the 10-digit part of the access code just used.
+    </li>
+    <li> Go to the general login page and enter your new credentials.
+    </li>
+  </ul>
+</div>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/studentaddpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/studentaddpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/studentaddpage.pt	(revision 6703)
@@ -0,0 +1,26 @@
+<h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+<form method="POST">
+  <table>
+    <tbody>
+      <tr>
+    	<td><label for="form.name">Full Name:</label></td>
+    	<td>
+         <input name="form.name" type="text" size="30" maxlength="30"
+                value="" />
+    	</td>
+      </tr>
+      <tr>
+        <td>
+            <div class="actionButtons" tal:condition="view/availableActions">
+                <span tal:repeat="action view/actions" tal:omit-tag="">
+                    <input tal:replace="structure action/render"/>
+                </span>
+            </div>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</form>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/studenthistory.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/studenthistory.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/studenthistory.pt	(revision 6703)
@@ -0,0 +1,9 @@
+<h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+<div class="workflow">
+  <div tal:repeat="msg context/history/messages">
+    <span tal:replace="msg">message</span><br />
+  </div>
+</div>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/studentpage.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/studentpage.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/browser_templates/studentpage.pt	(revision 6703)
@@ -0,0 +1,29 @@
+<h2 i18n:translate=""
+    tal:condition="view/label"
+    tal:content="view/label">Label</h2>
+
+<table class="zebra">
+  <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 class="controls">
+      <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/uli-studentpw/src/waeup/sirp/students/catalog.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/catalog.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/catalog.py	(revision 6703)
@@ -0,0 +1,57 @@
+"""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.index.text.parsetree import ParseError
+from waeup.sirp.interfaces import IUniversity, IQueryResultItem
+from waeup.sirp.students.interfaces import IStudent
+
+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')
+    name = index.Text(attribute='name')
+    reg_number = index.Field(attribute='reg_number')
+    state = index.Field(attribute='state')
+
+class StudentQueryResultItem(object):
+    grok.implements(IQueryResultItem)
+
+    title = u'Student Query Item'
+    description = u'Some students found in a search'
+
+    def __init__(self, context, view):
+        self.context = context
+        self.url = view.url(context)
+        self.student_id = context.student_id
+        self.name = context.name
+        self.reg_number = context.reg_number
+        self.state = context.state
+
+def search(query=None, searchtype=None, view=None):
+    hitlist = []
+    #if not query:
+    #    view.flash('Empty search string.')
+    #    return
+    if searchtype in ('name',):
+        results = Query().searchResults(
+            Text(('students_catalog', searchtype), query))
+    else:
+        # Temporary solution to display all students added
+        if query == '*':
+            from zope.component import queryUtility
+            from zope.catalog.interfaces import ICatalog
+            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
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/container.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/container.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/container.py	(revision 6703)
@@ -0,0 +1,82 @@
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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
+import os
+from zope.component.interfaces import IFactory
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from waeup.sirp.interfaces import IUserAccount
+from waeup.sirp.students.interfaces import (
+    IStudentsContainer, IStudent, IStudentPayments, IStudentAccommodation)
+from waeup.sirp.students.studycourse import StudentStudyCourse
+from waeup.sirp.students.payments import StudentPayments
+from waeup.sirp.students.accommodation import StudentAccommodation
+from waeup.sirp.utils.helpers import get_current_principal
+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')
+        student.clearance_locked = True
+        self[student.student_id] = student
+        studycourse = StudentStudyCourse()
+        self[student.student_id]['studycourse'] = studycourse
+        payments = StudentPayments()
+        self[student.student_id]['payments'] = payments
+        accommodation = StudentAccommodation()
+        self[student.student_id]['accommodation'] = accommodation
+        # Assign global student role for new student
+        account = IUserAccount(student)
+        account.roles = ['waeup.Student']
+        # Assign local StudentRecordOwner role
+        role_manager = IPrincipalRoleManager(self[student.student_id])
+        role_manager.assignRoleToPrincipal(
+            'waeup.local.StudentRecordOwner', student.student_id)
+        # Return student_id (only needed in tests)
+        return student.student_id
+
+    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.
+        """
+        user = get_current_principal()
+        if user is None:
+            user = 'system'
+        else:
+            user = user.id
+        self.logger.info('%s - %s - %s - %s' % (
+                user, ob_class, target, comment))
+        return
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/interfaces.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/interfaces.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/interfaces.py	(revision 6703)
@@ -0,0 +1,158 @@
+##
+## interfaces.py
+from zope.interface import Interface, Attribute
+from zope import schema
+from waeup.sirp.interfaces import IWAeUPObject
+from waeup.sirp.students.vocabularies import (
+  year_range, lgas_vocab, CertificateSource, GenderSource,
+  )
+
+class IStudentsContainer(IWAeUPObject):
+    """An 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(IWAeUPObject):
+    """Interface needed for student navigation.
+    """
+
+    def getStudent():
+        """Return student object.
+        """
+
+class IStudentBase(IWAeUPObject):
+    """Representation of student base data.
+    """
+    history = Attribute('Object history, a list of messages.')
+    state = Attribute('Returns the registration state of a student')
+    #student_id = Attribute('Randomly generated id')
+    password = Attribute('Encrypted password of a student')
+
+    def loggerInfo(ob_class, comment):
+        """Adds an INFO message to the log file
+        """
+
+    student_id = schema.TextLine(
+        title = u'Student ID',
+        required = True,
+        )
+
+    name = schema.TextLine(
+        title = u'Full Name',
+        default = u'Nobody',
+        required = True,
+        )
+
+    reg_number = schema.TextLine(
+        title = u'Registration Number',
+        default = u'',
+        required = True,
+        readonly = False,
+        )
+
+class IStudentClearance(IWAeUPObject):
+    """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,
+        )
+
+class IStudentPersonal(IWAeUPObject):
+    """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 IStudentStudyCourse(IWAeUPObject):
+    """A container for student study levels.
+
+    """
+
+    certificate = schema.Choice(
+        title = u'Certificate',
+        source = CertificateSource(),
+        default = None,
+        required = True,
+        )
+
+class IStudentAccommodation(IWAeUPObject):
+    """A container for student accommodation objects.
+
+    """
+
+class IStudentPayments(IWAeUPObject):
+    """A container for student payment objects.
+
+    """
+
+# Interfaces for students only
+
+class IStudentBaseEdit(IStudentBase):
+    """Interface needed for editing of student base data by students.
+    """
+
+    name = schema.TextLine(
+        title = u'Full Name',
+        default = u'Nobody',
+        required = True,
+        readonly = True,
+        )
+
+IStudentBaseEdit['name'].order =  IStudentBase['name'].order
+
+class IStudentClearanceEdit(IStudentClearance):
+    """Interface needed for restricted editing of student clearance data.
+    """
+
+class IStudentPersonalEdit(IStudentPersonal):
+    """Interface needed for restricted editing of student personal data.
+    """
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/lgas.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/lgas.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/lgas.py	(revision 6703)
@@ -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_uru_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-mopi','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/uli-studentpw/src/waeup/sirp/students/payments.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/payments.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/payments.py	(revision 6703)
@@ -0,0 +1,38 @@
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 contains the (student) payments objects.
+"""
+import grok
+from grok import index
+from zope.component.interfaces import IFactory
+from waeup.sirp.students.interfaces import IStudentPayments, IStudentNavigation
+from waeup.sirp.utils.helpers import attrs_to_fields
+
+class StudentPayments(grok.Container):
+    """This is a container for student payments.
+    """
+    grok.implements(IStudentPayments, IStudentNavigation)
+    grok.provides(IStudentPayments)
+
+    def __init__(self):
+        super(StudentPayments, self).__init__()
+        return
+
+    def getStudent(self):
+        return self.__parent__
+
+StudentPayments = attrs_to_fields(StudentPayments)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/permissions.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/permissions.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/permissions.py	(revision 6703)
@@ -0,0 +1,47 @@
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 ManageStudents(grok.Permission):
+    grok.name('waeup.manageStudents')
+
+# Local role
+class StudentRecordOwner(grok.Role):
+    grok.name('waeup.local.StudentRecordOwner')
+    grok.title(u'Student Record Owner')
+    grok.permissions('waeup.handleStudent', 'waeup.viewStudent')
+
+# Global Roles
+class StudentRole(grok.Role):
+    grok.name('waeup.Student')
+    grok.permissions('waeup.Public', 'waeup.View')
+
+class StudentsOfficer(grok.Role):
+    grok.name('waeup.StudentsOfficer')
+    grok.title(u'Students Officer')
+    grok.permissions('waeup.View', 'waeup.Public',
+                     'waeup.viewStudent', 'waeup.manageStudents')
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/student.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/student.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/student.py	(revision 6703)
@@ -0,0 +1,85 @@
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 grok
+from grok import index
+from zope.component.interfaces import IFactory, ComponentLookupError
+from zope.interface import implementedBy
+from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
+from waeup.sirp.interfaces import IObjectHistory
+from waeup.sirp.students.interfaces import (
+    IStudent, IStudentNavigation, IStudentBaseEdit, IStudentClearanceEdit,
+    IStudentPersonalEdit)
+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,
+        IStudentBaseEdit, IStudentClearanceEdit, IStudentPersonalEdit)
+    grok.provides(IStudent)
+
+    def __init__(self):
+        super(Student, self).__init__()
+        IWorkflowInfo(self).fireTransition('create')
+        # The catalog doesn't exist in unit tests
+        try:
+            self.student_id = generate_student_id('?')
+        except ComponentLookupError:
+            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 state(self):
+        state = IWorkflowState(self).getState()
+        return state
+
+    @property
+    def history(self):
+        history = IObjectHistory(self)
+        return history
+
+    def getStudent(self):
+        return self
+
+# 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)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/studycourse.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/studycourse.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/studycourse.py	(revision 6703)
@@ -0,0 +1,40 @@
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public 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 grok import index
+from zope.component.interfaces import IFactory
+from waeup.sirp.students.interfaces import (
+    IStudentStudyCourse, IStudentNavigation)
+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__
+
+StudentStudyCourse = attrs_to_fields(StudentStudyCourse)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/__init__.py	(revision 6703)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/test_authentication.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/test_authentication.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/test_authentication.py	(revision 6703)
@@ -0,0 +1,153 @@
+##
+## test_authentication.py
+## Login : <uli@pu.smp.net>
+## Started on  Fri Sep  2 15:25:56 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
+##
+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.students.authentication import (
+    StudentsAuthenticatorSetup, StudentAccount)
+
+
+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'
+    name = 'Test User'
+    password = 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_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
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/test_browser.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/test_browser.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/test_browser.py	(revision 6703)
@@ -0,0 +1,288 @@
+##
+## test_browser.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Mar 29 11:31:11 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public 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
+from mechanize import LinkNotFoundError
+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 hurry.workflow.interfaces import IWorkflowInfo
+from waeup.sirp.testing import (
+    FunctionalLayer, FunctionalTestCase, remove_logger
+    )
+from waeup.sirp.app import University
+from waeup.sirp.students.container import StudentsContainer
+from waeup.sirp.students.student import Student
+from waeup.sirp.university.faculty import Faculty
+from waeup.sirp.university.department import Department
+from waeup.sirp.interfaces import IUserAccount
+
+PH_LEN = 2059  # Length of placeholder file
+
+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 (done by addStudent)
+        student = Student()
+        student.name = u'Anna Tester'
+        self.test_student_id = self.app['students'].addStudent(student)
+        # Set password
+        IUserAccount(self.app['students'][self.test_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.test_student_id
+        self.manage_student_path = self.student_path + '/edit_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.accommodation_student_path = self.student_path + '/accommodation'
+        self.history_student_path = self.student_path + '/history'
+
+        # Populate university
+        certificate = createObject('waeup.Certificate')
+        certificate.code = 'CERT1'
+        certificate.application_category = 'basic'
+        self.app['faculties']['fac1'] = Faculty()
+        self.app['faculties']['fac1']['dep1'] = Department()
+        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
+
+    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.name").value = 'Bob Tester'
+        self.browser.getControl("Create student record").click()
+        self.assertTrue('Student record created' in self.browser.contents)
+
+        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.test_student_id
+        self.browser.getControl("Search").click()
+        self.assertTrue('Anna Tester' in self.browser.contents)
+
+        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 = ['name']
+        self.browser.getControl(name="searchterm").value = 'Anna Tester'
+        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.test_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.test_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.test_student_id
+        self.browser.getControl("Search").click()
+        self.assertTrue('No student found' in self.browser.contents)
+
+class StudentUITests(StudentsFullSetup):
+    # Tests for Student class views and pages
+
+    layer = FunctionalLayer
+
+    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.name").value = 'John Tester'
+        self.browser.getControl(name="form.reg_number").value = '123'
+        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.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("Manage").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.edit_personal_student_path)
+
+        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.browser.getLink("Manage").click()
+        self.assertTrue('Manage study course' in self.browser.contents)
+
+        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.accommodation_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 zope.mgr...',
+                           self.browser.contents)
+        return
+
+    def test_student_access(self):
+        # Students can access their own objects
+        # and can perform actions
+        remove_logger('waeup.sirp.app.students')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.test_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="password").value = 'new_password'
+        self.browser.getControl(name="control_password").value = 'new_password'
+        self.browser.getControl("Save").click()
+        self.assertTrue('Form has been saved' in self.browser.contents)
+        # We are still logged in. Changing the password hasn't thrown us out.
+        self.browser.getLink("My 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.test_student_id
+        self.browser.getControl(name="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
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/test_catalog.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/test_catalog.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/test_catalog.py	(revision 6703)
@@ -0,0 +1,88 @@
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public 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
+import unittest
+from zope.app.testing.functional import (
+    FunctionalTestCase, FunctionalTestSetup, getRootFolder)
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility
+from zope.component.hooks import setSite, clearSite
+from zope.interface.verify import verifyClass, verifyObject
+from hurry.query.interfaces import IQuery
+from hurry.query import Eq
+from waeup.sirp.app import University
+from waeup.sirp.interfaces import IQueryResultItem
+from waeup.sirp.testing import FunctionalLayer
+from waeup.sirp.students.student import Student
+#from waeup.sirp.students.workflow import INITIALIZED, USED, DISABLED
+from waeup.sirp.students.catalog import StudentQueryResultItem, search
+
+class CatalogTestSetup(FunctionalTestCase):
+    # A setup for testing students 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)
+
+        # Create students
+        student = Student()
+        student.name = u'Bob Tester'
+        student.student_id = u'A123456'
+        self.app['students'][student.student_id] = student
+        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=('A123456', 'A123456'))
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students']['A123456']
+
+    def test_search_by_name(self):
+        # We can find a certain name
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(name='Bob Tester')
+        results = [x for x in results] # Turn results generator into list
+        assert len(results) == 1
+        assert results[0] is self.app['students']['A123456']
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/test_container.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/test_container.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/test_container.py	(revision 6703)
@@ -0,0 +1,102 @@
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public 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 unittest
+import tempfile
+import shutil
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component.interfaces import IFactory
+from zope.component.hooks import setSite, clearSite
+from waeup.sirp.app import University
+from waeup.sirp.students.interfaces import (
+    IStudentsContainer,
+    )
+from waeup.sirp.students.container import (
+    StudentsContainer, 
+    )
+from waeup.sirp.testing import (
+    FunctionalLayer, FunctionalTestCase, get_all_loggers, remove_new_loggers,
+    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)
+
+    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/uli-studentpw/src/waeup/sirp/students/tests/test_student.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/test_student.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/tests/test_student.py	(revision 6703)
@@ -0,0 +1,87 @@
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Tests for students and related.
+"""
+import unittest
+from StringIO import StringIO
+from zope.app.testing.functional import FunctionalTestCase
+from zope.component import (
+    provideAdapter, adapts, getGlobalSiteManager, provideUtility)
+from zope.component.hooks import setSite
+from zope.component.interfaces import IFactory
+from zope.interface import verify, implements
+from zope.location.interfaces import ILocation
+from zope.publisher.base import TestRequest
+from zope.publisher.interfaces import NotFound
+from waeup.sirp.students.student import (
+    Student, StudentFactory,
+    )
+from waeup.sirp.students.studycourse import StudentStudyCourse
+from waeup.sirp.students.payments import StudentPayments
+from waeup.sirp.students.accommodation import StudentAccommodation
+from waeup.sirp.students.interfaces import (
+    IStudent, IStudentStudyCourse, IStudentPayments, IStudentAccommodation,
+    )
+from waeup.sirp.testing import FunctionalLayer
+
+class StudentTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentTest, self).setUp()
+        self.student = Student()
+        self.studycourse = StudentStudyCourse()
+        self.payments = StudentPayments()
+        self.accommodation = StudentAccommodation()
+        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.verifyClass(IStudentPayments, StudentPayments)
+        verify.verifyObject(IStudentPayments, self.payments)
+        verify.verifyClass(IStudentAccommodation, StudentAccommodation)
+        verify.verifyObject(IStudentAccommodation, self.accommodation)
+        return
+
+class StudentFactoryTest(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        self.factory = StudentFactory()
+
+    def tearDown(self):
+        pass
+
+    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/uli-studentpw/src/waeup/sirp/students/utils.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/utils.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/utils.py	(revision 6703)
@@ -0,0 +1,14 @@
+"""General helper functions for the student section.
+"""
+
+from random import SystemRandom as r
+from waeup.sirp.students.catalog import search
+
+def generate_student_id(letter):
+    # XXX: using catalog search here might be overkill?
+    if letter == '?':
+        letter= r().choice('ABCDEFGHKLMNPQRSTUVWXY')
+    sid = u"%c%d" % (letter,r().randint(99999,1000000))
+    while search(query=sid,searchtype='student_id'):
+        sid = u"%c%d" % (letter,r().randint(99999,1000000))
+    return sid
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/viewlets.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/viewlets.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/viewlets.py	(revision 6703)
@@ -0,0 +1,191 @@
+import grok
+from zope.interface import Interface
+from waeup.sirp.interfaces import IWAeUPObject
+
+grok.context(IWAeUPObject) # Make IWAeUPObject the default context
+grok.templatedir('browser_templates')
+
+class StudentManageSidebar(grok.ViewletManager):
+    grok.name('left_studentmanage')
+
+class StudentManageLink(grok.Viewlet):
+    """A link displayed in the student box which shows up for StudentNavigation
+    objects.
+
+    """
+    grok.baseclass()
+    grok.viewletmanager(StudentManageSidebar)
+    grok.context(IWAeUPObject)
+    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)
+        return u'<div class="portlet"><a href="%s">%s</a></div>' % (
+                url, self.text)
+
+class StudentManageBaseLink(StudentManageLink):
+    grok.order(1)
+    link = 'index'
+    text = u'Base Data'
+
+class StudentManageClearanceLink(StudentManageLink):
+    grok.order(2)
+    link = 'view_clearance'
+    text = u'Clearance Data'
+
+class StudentManagePersonalLink(StudentManageLink):
+    grok.order(2)
+    link = 'view_personal'
+    text = u'Personal Data'
+
+class StudentManageStudyCourseLink(StudentManageLink):
+    grok.order(3)
+    link = 'studycourse'
+    text = u'Study Course'
+
+class StudentManagePaymentsLink(StudentManageLink):
+    grok.order(4)
+    link = 'payments'
+    text = u'Payments'
+
+class StudentManageAccommodationLink(StudentManageLink):
+    grok.order(5)
+    link = 'accommodation'
+    text = u'Accommodation Data'
+
+class StudentManageHistoryLink(StudentManageLink):
+    grok.order(6)
+    link = 'history'
+    text = u'History'
+
+
+class StudentMenu(grok.ViewletManager):
+    grok.name('top_student')
+
+class StudentLink(grok.Viewlet):
+    """A link displayed in the student box which shows up for StudentNavigation
+    objects.
+
+    """
+    grok.baseclass()
+    grok.viewletmanager(StudentMenu)
+    grok.context(IWAeUPObject)
+    grok.view(Interface)
+    grok.order(5)
+    grok.require('waeup.viewStudent')
+    template = grok.PageTemplateFile('browser_templates/plainactionbutton.pt')
+
+    link = 'index'
+    text = u'Base Data'
+
+    @property
+    def alt(self):
+        """Alternative text for icon.
+        """
+        return self.text
+
+    @property
+    def target_url(self):
+        """Get a URL to the target...
+        """
+        return self.view.url(self.context.getStudent(), self.link)
+
+class StudentBaseLink(StudentLink):
+    grok.order(1)
+    link = 'index'
+    text = u'Base Data'
+
+class StudentClearanceLink(StudentLink):
+    grok.order(2)
+    link = 'view_clearance'
+    text = u'Clearance Data'
+
+class StudentPersonalLink(StudentLink):
+    grok.order(2)
+    link = 'view_personal'
+    text = u'Personal Data'
+
+class StudentStudyCourseLink(StudentLink):
+    grok.order(3)
+    link = 'studycourse'
+    text = u'Study Course'
+
+class StudentPaymentsLink(StudentLink):
+    grok.order(4)
+    link = 'payments'
+    text = u'Payments'
+
+class StudentAccommodationLink(StudentLink):
+    grok.order(5)
+    link = 'accommodation'
+    text = u'Accommodation'
+
+class StudentHistoryLink(StudentLink):
+    grok.order(6)
+    link = 'history'
+    text = u'History'
+
+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)
+    grok.template('primarynavtab')
+    grok.order(1)
+    grok.require('waeup.View')
+    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 HomeTab(PrimaryStudentNavTab):
+    """Home-tab in primary navigation.
+    """
+    grok.order(1)
+    grok.require('waeup.Public')
+    pnav = 0
+    tab_title = u'Home'
+
+class ProspectusTab(PrimaryStudentNavTab):
+    """Faculties-tab in primary navigation.
+    """
+    grok.order(2)
+    grok.require('waeup.View')
+    pnav = 1
+    tab_title = u'Prospectus'
+
+    @property
+    def link_target(self):
+        return self.view.application_url('faculties')
+
+class MyDataTab(PrimaryStudentNavTab):
+    """MyData-tab in primary navigation.
+    """
+    grok.order(3)
+    grok.require('waeup.Public')
+    pnav = 4
+    tab_title = u'My Data'
+
+    @property
+    def link_target(self):
+        rel_link = '/students/%s' % self.request.principal.id
+        return self.view.application_url() + rel_link
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/vocabularies.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/vocabularies.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/vocabularies.py	(revision 6703)
@@ -0,0 +1,61 @@
+"""Vocabularies and sources for the student section.
+"""
+from datetime import datetime
+from zope.component import getUtility
+from zope.catalog.interfaces import ICatalog
+from zc.sourcefactory.basic import BasicSourceFactory
+from zc.sourcefactory.contextual import BasicContextualSourceFactory
+from waeup.sirp.interfaces import SimpleWAeUPVocabulary
+from waeup.sirp.students.lgas import LGAS
+
+def year_range():
+    curr_year = datetime.now().year
+    return range(curr_year - 2, curr_year + 5)
+
+def entry_sessions():
+    curr_year = datetime.now().year
+    year_range = range(curr_year - 5, curr_year + 2)
+    return [('%s/%s' % (year,year+1), '%s' % year) for year in year_range]
+
+entry_session_vocab = SimpleWAeUPVocabulary(*entry_sessions())
+
+lgas_vocab = SimpleWAeUPVocabulary(
+    *sorted([(x[1],x[0]) for x in LGAS]))
+
+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'
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/workflow.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/workflow.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/students/workflow.py	(revision 6703)
@@ -0,0 +1,74 @@
+"""Workflow for students.
+"""
+import grok
+from datetime import datetime
+from hurry.workflow.workflow import Transition, WorkflowState, NullCondition
+from hurry.workflow.interfaces import IWorkflowState, IWorkflowTransitionEvent
+from waeup.sirp.students.interfaces import IStudent
+from waeup.sirp.interfaces import IObjectHistory, IWAeUPWorkflowInfo
+from waeup.sirp.workflow import WAeUPWorkflow, WAeUPWorkflowInfo
+from waeup.sirp.utils.helpers import get_current_principal
+
+CREATED = 'created'
+ADMITTED = 'admitted'
+
+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),
+    )
+
+registration_workflow = WAeUPWorkflow(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(WAeUPWorkflowInfo, grok.Adapter):
+    """Adapter to adapt Student objects to workflow info objects.
+    """
+    grok.context(IStudent)
+    grok.provides(IWAeUPWorkflowInfo)
+
+    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.
+    """
+    msg = '%s' % event.transition.user_data['msg']
+    history = IObjectHistory(obj)
+    history.addMessage(msg)
+    # In some tests we don't have a students container or a user
+    try:
+        user = get_current_principal()
+        students_container = grok.getSite()['students']
+        students_container.logger.info('%s - %s - %s' % (user.id,obj.student_id,msg))
+    except (TypeError, AttributeError):
+        pass
+    return
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/testing.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/testing.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/testing.py	(revision 6703)
@@ -0,0 +1,312 @@
+"""Testing support for :mod:`waeup.sirp`.
+"""
+import grok
+import doctest
+import logging
+import os.path
+import re
+import warnings
+import zope.component
+import waeup.sirp
+from zope.app.testing.functional import (
+    ZCMLLayer, FunctionalTestSetup, getRootFolder, sync, FunctionalTestCase)
+from zope.component import getGlobalSiteManager
+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.
+    """
+    return logging.root.manager.loggerDict.keys()
+
+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.WAeUPSIRPUnitTestLayer`.
+
+    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.WAeUPSIRPUnitTestLayer`
+
+    """
+    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
+
+
+class WAeUPSIRPUnitTestLayer(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 WAeUPSIRPUnitTestLayer
+
+      class MyTestCase(unittest.TestCase):
+
+          layer = WAeUPSIRPUnitTestLayer
+
+          # 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()
+        if grokked:
+            pass
+            #setUpZope(None)
+        return
+
+    @classmethod
+    def tearDown(cls):
+        cleanUpZope(None)
+
+#: 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>'),
+    ])
+
+def setUp(test):
+    FunctionalTestSetup().setUp()
+
+def tearDown(test):
+    FunctionalTestSetup().tearDown()
+
+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)
+
+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)
+        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)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/__init__.py	(revision 6703)
@@ -0,0 +1,2 @@
+# This is a package
+
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/test_app.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/test_app.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/test_app.py	(revision 6703)
@@ -0,0 +1,53 @@
+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.imagestorage import ImageStorageFileRetrieval
+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_images(self):
+        # Make sure we have a image container in a university
+        assert 'images' in self.app.keys()
+        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)
+        assert isinstance(result, ImageStorageFileRetrieval)
+        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/uli-studentpw/src/waeup/sirp/tests/test_authentication.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/test_authentication.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/test_authentication.py	(revision 6703)
@@ -0,0 +1,70 @@
+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, PrincipalInfo, 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, PrincipalInfo))
+        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, PrincipalInfo))
+        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/uli-studentpw/src/waeup/sirp/tests/test_imagestorage.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/test_imagestorage.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/test_imagestorage.py	(revision 6703)
@@ -0,0 +1,302 @@
+import os
+import sys
+import tempfile
+import shutil
+import unittest
+from StringIO import StringIO
+from hurry.file.interfaces import IFileRetrieval
+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.image import createWAeUPImageFile
+from waeup.sirp.imagestorage import (
+    md5digest, Basket, ImageStorage, ImageStorageFileRetrieval,)
+
+class HelperFuncsTests(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.workdir)
+
+    def test_md5digest(self):
+        samplefile = os.path.join(self.workdir, 'sample')
+        open(samplefile, 'wb').write('blah')
+        fp = open(samplefile, 'r')
+        digest = md5digest(fp)
+        self.assertEqual(digest, '6f1ed002ab5595859014ebf0951522d9')
+
+class BasketTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(BasketTests, 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.basket = Basket()
+        self.fd = open(self.samplefile, 'r')
+        self.fd2 = open(self.otherfile, 'r')
+        self.stderr = StringIO()
+        self.old_stderr = sys.stderr
+
+    def tearDown(self):
+        sys.stderr = self.old_stderr
+        super(BasketTests, self).tearDown()
+        self.fd.close()
+        shutil.rmtree(self.workdir)
+        self.basket._del() # Remove subojects explicitly
+        del self.basket
+        return
+
+    def test_ifaces(self):
+        pass
+
+    def test_curr_id_empty(self):
+        curr_id = self.basket.curr_id
+        self.assertEqual(curr_id, '1')
+
+    def test_getInternalId_empty(self):
+        basket_id = self.basket.getInternalId(self.fd)
+        self.assertTrue(basket_id is None)
+
+    def test_storeFile_single(self):
+        basket_id = self.basket.storeFile(self.fd, 'sample')
+        self.assertEqual(basket_id, '1')
+        contents = self.basket['1'].open('r').read()
+        self.assertEqual(contents, 'Hi there!')
+
+    def test_storeFile_double(self):
+        basket_id1 = self.basket.storeFile(self.fd, 'sample')
+        basket_id2 = self.basket.storeFile(self.fd, 'sample')
+        self.assertTrue(basket_id1 == basket_id2 == '1')
+        contents = self.basket['1'].open('r').read()
+        self.assertEqual(contents, 'Hi there!')
+
+    def test_storeFile_multiple(self):
+        basket_id1 = self.basket.storeFile(self.fd, 'sample')
+        basket_id2 = self.basket.storeFile(self.fd2, 'sample')
+        self.assertEqual(basket_id1, '1')
+        self.assertEqual(basket_id2, '2')
+        contents1 = self.basket['1'].open('r').read()
+        contents2 = self.basket['2'].open('r').read()
+        self.assertEqual(contents1, 'Hi there!')
+        self.assertEqual(contents2, 'Hi from other!')
+
+    def test_retrieveFile(self):
+        basket_id = self.basket.storeFile(self.fd, 'sample')
+        fd = self.basket.retrieveFile(basket_id)
+        result = fd.read()
+        self.assertEqual(result, 'Hi there!')
+
+    def test_retrieveFile_not_existent(self):
+        result = self.basket.retrieveFile('not-a-valid-basket-id')
+        self.assertTrue(result is None)
+
+    def test_detect_zero_length_blobs(self):
+        # Ensure we get a warning when an empty Blob is found
+        self.basket.storeFile(self.fd, 'sample')
+        self.basket['1'].open('w').write('')
+        self.fd.seek(0)
+        sys.stderr = self.stderr         # Redirect stderr
+        self.basket.storeFile(self.fd, 'sample')
+        sys.stderr = self.old_stderr     # Restore stderr
+        self.stderr.seek(0)
+        self.assertTrue(
+            "EMPTY BLOB DETECTED" in self.stderr.read())
+
+    def test_refill_zero_length_blobs(self):
+        # When we detect an empty Blob, it will be reused
+        self.basket.storeFile(self.fd, 'sample')
+        self.basket['1'].open('w').write('')
+        self.fd.seek(0)
+        sys.stderr = self.stderr         # Redirect stderr
+        self.basket.storeFile(self.fd, 'sample')
+        sys.stderr = self.old_stderr     # Restore stderr
+        contents = self.basket['1'].open('r').read()
+        self.assertEqual(contents, 'Hi there!')
+
+
+class ImageStorageTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ImageStorageTests, 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.storage = ImageStorage()
+        self.fd = open(self.samplefile, 'r')
+        self.fd2 = open(self.otherfile, 'r')
+
+    def tearDown(self):
+        super(ImageStorageTests, self).tearDown()
+        self.fd.close()
+        self.fd2.close()
+        shutil.rmtree(self.workdir)
+        self.storage._del() # Remove subojects explicitly
+        del self.storage
+        return
+
+    def test_ifaces(self):
+        pass
+
+    def test_storeFile(self):
+        full_id = self.storage.storeFile(self.fd, 'sample.txt')
+        self.assertEqual(full_id, '396199333edbf40ad43e62a1c1397793-1')
+
+    def test_storeFile_duplicate(self):
+        full_id1 = self.storage.storeFile(self.fd, 'sample1.txt')
+        full_id2 = self.storage.storeFile(self.fd, 'sample2.txt')
+        full_id3 = self.storage.storeFile(self.fd, 'sample1.txt')
+        self.assertEqual(full_id1, '396199333edbf40ad43e62a1c1397793-1')
+        self.assertEqual(full_id2, '396199333edbf40ad43e62a1c1397793-1')
+        self.assertEqual(full_id3, '396199333edbf40ad43e62a1c1397793-1')
+        contents = self.storage.retrieveFile(
+            '396199333edbf40ad43e62a1c1397793-1').read()
+        self.assertEqual(contents, 'Hi there!')
+
+    def test_storeFile_multiple(self):
+        full_id1 = self.storage.storeFile(self.fd, 'sample1.txt')
+        full_id2 = self.storage.storeFile(self.fd2, 'sample1.txt')
+        self.assertEqual(full_id1, '396199333edbf40ad43e62a1c1397793-1')
+        self.assertEqual(full_id2, '6936fcf8d564f1c5be5a017e650c5e8f-1')
+
+    def test_retrieveFile_not_existent(self):
+        result = self.storage.retrieveFile('not-existent')
+        self.assertTrue(result is None)
+
+    def test_retrieveFile_illegal_marker(self):
+        result1 = self.storage.retrieveFile('really-not-existent')
+        result2 = self.storage.retrieveFile('notexistent')
+        self.assertTrue(result1 is result2 is None)
+
+    def test_retrieveFile(self):
+        full_id = self.storage.storeFile(self.fd, 'sample.txt')
+        result = self.storage.retrieveFile(full_id)
+        content = result.read()
+        self.assertEqual(content, 'Hi there!')
+
+    def test_retrieveFile_multiple(self):
+        full_id1 = self.storage.storeFile(self.fd, 'sample.txt')
+        full_id2 = self.storage.storeFile(self.fd2, 'other.txt')
+        result1 = self.storage.retrieveFile(full_id1)
+        result2 = self.storage.retrieveFile(full_id2)
+        content1 = result1.read()
+        content2 = result2.read()
+        self.assertEqual(content1, 'Hi there!')
+        self.assertEqual(content2, 'Hi from other!')
+
+class ImageStorageFileRetrievalTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(ImageStorageFileRetrievalTests, 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.storage = ImageStorage()
+        self.fd = open(self.samplefile, 'r')
+        self.fd2 = open(self.otherfile, 'r')
+        # Set up a single image storage in a site
+        self.getRootFolder()['app'] = University()
+        self.app = self.getRootFolder()['app']
+        if not 'images' in self.app.keys():
+            self.app['images'] = ImageStorage()
+        self.storage = self.app['images']
+        return
+
+
+    def tearDown(self):
+        super(ImageStorageFileRetrievalTests, self).tearDown()
+        self.fd.close()
+        self.fd2.close()
+        shutil.rmtree(self.workdir)
+        self.storage._del() # Remove subojects explicitly
+        return
+
+    def test_ifaces(self):
+        retrieval = ImageStorageFileRetrieval()
+        assert verifyClass(IFileRetrieval, ImageStorageFileRetrieval)
+        assert verifyObject(IFileRetrieval, retrieval)
+        return
+
+    def test_getImageStorage_nosite(self):
+        retrieval = ImageStorageFileRetrieval()
+        storage = retrieval.getImageStorage()
+        self.assertTrue(storage is None)
+        return
+
+    def test_getImageStorage(self):
+        setSite(self.app)
+        retrieval = ImageStorageFileRetrieval()
+        storage = retrieval.getImageStorage()
+        self.assertTrue(storage is self.storage)
+        return
+
+    def test_isImageStorageEnabled_nosite(self):
+        retrieval = ImageStorageFileRetrieval()
+        self.assertTrue(retrieval.isImageStorageEnabled() is False)
+        return
+
+    def test_isImageStorageEnabled(self):
+        setSite(self.app)
+        retrieval = ImageStorageFileRetrieval()
+        self.assertTrue(retrieval.isImageStorageEnabled() is True)
+        return
+
+    def test_getFile_nosite(self):
+        retrieval = ImageStorageFileRetrieval()
+        f = retrieval.getFile('Hi there!')
+        self.assertEqual(f.read(), 'Hi there!')
+        return
+
+    def test_getFile(self):
+        setSite(self.app)
+        retrieval = ImageStorageFileRetrieval()
+        waeup_image = retrieval.createFile('sample.txt', self.fd)
+        full_id = waeup_image.data
+        result = retrieval.getFile(full_id)
+        self.assertEqual(result.read(), 'Hi there!')
+        return
+
+    def test_createFile_nosite(self):
+        retrieval = ImageStorageFileRetrieval()
+        waeup_image = retrieval.createFile('sample.txt', self.fd)
+        self.assertEqual(waeup_image.data, 'Hi there!')
+        return
+
+    def test_createFile(self):
+        # Ensure we can create WAeUPImageFiles when in site
+        setSite(self.app)
+        retrieval = ImageStorageFileRetrieval()
+        waeup_image = retrieval.createFile('sample.txt', self.fd)
+        full_id = waeup_image.data
+        self.assertEqual(full_id, '396199333edbf40ad43e62a1c1397793-1')
+        return
+
+    def test_waeupimagefile(self):
+        # Make sure WAeUPImageFile can use our file retrieval
+        setSite(self.app)
+        myfile = createWAeUPImageFile('sample.jpg', self.fd)
+        contents = myfile.file.read()
+        self.assertEqual(contents, 'Hi there!')
+
+    def test_waeupimagefile_raw(self):
+        # Make sure we can retrieve a file also if it was initialized
+        # with no image storage available
+        myfile = createWAeUPImageFile('sample.jpg', self.fd)
+        setSite(self.app)
+        contents = myfile.file.read()
+        self.assertEqual(contents, 'Hi there!')
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/test_interfaces.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/test_interfaces.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/test_interfaces.py	(revision 6703)
@@ -0,0 +1,56 @@
+# 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 NonWAeUPRole(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('NonWAeUPRole', NonWAeUPRole)
+        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/uli-studentpw/src/waeup/sirp/tests/test_objecthistory.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/test_objecthistory.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/test_objecthistory.py	(revision 6703)
@@ -0,0 +1,83 @@
+"""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, IWAeUPObject
+from waeup.sirp.testing import FunctionalTestCase, FunctionalLayer
+from waeup.sirp.objecthistory import ObjectHistory
+
+class SampleObject(grok.Model):
+    grok.implements(IWAeUPObject)
+    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/uli-studentpw/src/waeup/sirp/tests/test_waeup.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/test_waeup.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/test_waeup.py	(revision 6703)
@@ -0,0 +1,22 @@
+##
+## test_waeuptest.py
+"""
+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/uli-studentpw/src/waeup/sirp/tests/util.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/util.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/tests/util.py	(revision 6703)
@@ -0,0 +1,27 @@
+
+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/uli-studentpw/src/waeup/sirp/university/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/__init__.py	(revision 6703)
@@ -0,0 +1,7 @@
+# Make this a package.
+from waeup.sirp.university.course import Course
+from waeup.sirp.university.coursecontainer import CourseContainer
+from waeup.sirp.university.faculty import Faculty
+from waeup.sirp.university.facultycontainer import FacultyContainer
+__all__ = (
+    'Course', 'CourseContainer', 'Faculty', 'FacultyContainer')
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/api.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/api.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/api.txt	(revision 6703)
@@ -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
+   certificatecontainer.txt
+   course.txt
+   coursecontainer.txt
+   department.txt
+   faculty.txt
+   facultycontainer.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/uli-studentpw/src/waeup/sirp/university/batching.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/batching.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/batching.py	(revision 6703)
@@ -0,0 +1,263 @@
+"""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 (
+    IFacultyContainer, 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['core_or_elective'])
+        return
+
+    def delEntry(self, row, site):
+        parent = self.getParent(row, site)
+        parent.delCourseRef(row['course'].code, row['level'])
+        return
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/catalog.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/catalog.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/catalog.py	(revision 6703)
@@ -0,0 +1,138 @@
+"""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.index import UniqueField
+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 = UniqueField(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 = UniqueField(attribute='code')
+    application_category = grok.index.Field(attribute='application_category')
+
+class CertificateCoursesIndexes(grok.Indexes):
+    """This catalog is needed for automatic removal of certificate courses
+    and later for selection course tickets in the students section.
+    """
+    grok.site(IUniversity)
+    grok.name('certcourses_catalog')
+    grok.context(ICertificateCourse)
+
+    course_code = grok.index.Field(attribute='getCourseCode')
+
+@grok.subscribe(ICourse, grok.IObjectAddedEvent)
+def handleCourseAdd(obj, event):
+    """Index an added 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 handleCertificateAdd(obj, event):
+    """Index an added certificates 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(IDepartment, grok.IObjectRemovedEvent)
+def handleDepartmentRemoval(obj, event):
+    """Clear courses and certificates when a department is killed.
+    """
+    obj.courses.clear()
+    obj.certificates.clear()
+    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 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(('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/uli-studentpw/src/waeup/sirp/university/certificate.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/certificate.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/certificate.py	(revision 6703)
@@ -0,0 +1,118 @@
+"""WAeUP portal certificates
+"""
+import grok
+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, ICourse)
+from waeup.sirp.university.vocabularies import course_levels    
+
+class Certificate(grok.Container):
+    """A certificate.
+    """
+    grok.implements(ICertificate, ICertificateAdd)
+
+    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, core_or_elective=True):
+        """Add a course referrer.
+        """
+        code = "%s_%s" % (course.code, level)
+        self[code] = CertificateCourse(course, level, core_or_elective)
+        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.
+        """
+        for key in self.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, core_or_elective=True):
+        self.course = course
+        self.level = level
+        self.core_or_elective = core_or_elective
+
+    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(ICourse, grok.IObjectRemovedEvent)
+def removedCourseHandler(course, event):
+    """If a course is deleted, we make sure that also referrers in a
+       certificatecontainer 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/uli-studentpw/src/waeup/sirp/university/certificate.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/certificate.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/certificate.txt	(revision 6703)
@@ -0,0 +1,366 @@
+:mod:`waeup.sirp.university.certificate` -- Certificates for WAeUP
+******************************************************************
+
+.. module:: waeup.sirp.university.certificate
+
+Components that represent and manage certificates.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
+
+
+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,[ core_or_elective=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[, core_or_elective=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:: core_or_elective
+
+      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/uli-studentpw/src/waeup/sirp/university/certificatecontainer.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/certificatecontainer.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/certificatecontainer.py	(revision 6703)
@@ -0,0 +1,114 @@
+"""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 (
+    ICertificateContainer, ICertificate)
+
+class CertificateContainer(grok.Container):
+    """A storage for certificates.
+
+    A :class:`CertificateContainer` 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:`CertificateContainer` provides
+    :iface:`ICertificateContainer`.
+    """
+    grok.implements(ICertificateContainer)
+    grok.require('waeup.manageUniversity')
+
+    def __setitem__(self, name, certificate):
+        """Insert `certificate` with `name` as key into container.
+
+        The `certificate` must be an object implementing
+        :iface:`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('CertificateContainers 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(CertificateContainer, self).__setitem__(name, certificate)
+
+    def addCertificate(self, certificate):
+        """Add `certificate` to the container.
+
+        The certificate must be an object implementing
+        :iface:`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 CertificateContainerFactory(grok.GlobalUtility):
+    """A factory for certificate containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.CertificateContainer')
+    title = u"Create a new certificate container.",
+    description = u"This factory instantiates new certificate containers."
+
+    def __call__(self, *args, **kw):
+        return CertificateContainer(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(CertificateContainer)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/certificatecontainer.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/certificatecontainer.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/certificatecontainer.txt	(revision 6703)
@@ -0,0 +1,149 @@
+:mod:`waeup.sirp.university.certificatecontainer` -- Certificate containers
+***************************************************************************
+
+.. module:: waeup.sirp.university.certificatecontainer
+
+Containers for certificates.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
+
+
+Content Classes (models and containers)
+=======================================
+
+:class:`CertificateContainer`
+-----------------------------
+
+.. class:: CertificateContainer()
+
+   Create a certificate container instance.
+
+   .. attribute:: grok.implements(ICertificateContainer)
+
+   .. attribute:: grok.require('waeup.manageUniversity')
+
+   :class:`CertificateContainer` instances are instances of
+   :class:`grok.Container` that hold and manage instances of
+   :class:`waeup.sirp.university.certificate.Certificate`.
+
+     >>> from waeup.sirp.university.certificatecontainer import CertificateContainer
+     >>> mycontainer = CertificateContainer()
+     >>> mycontainer
+     <waeup.sirp.university.certificatecontainer.CertificateContainer object at 0x...>
+
+   Certificate containers provide `ICertificateContainer`:
+
+     >>> from waeup.sirp.university.interfaces import ICertificateContainer
+     >>> ICertificateContainer.providedBy(mycontainer)
+     True
+
+   We can be sure, that the full interface is supported by the
+   CertificateContainer class:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> verifyClass(ICertificateContainer, CertificateContainer)
+     True
+
+   As normal ``grok.Container``, :class:`CertificateContainer` 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.certificatecontainer import (
+        ...   CertificateContainer)
+        >>> mycontainer = CertificateContainer()
+        >>> mycontainer.addCertificate('blah')
+        Traceback (most recent call last):
+        ...
+        TypeError: CertificateContainers 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:`CertificateContainerFactory`
+------------------------------------
+
+.. class:: CertificateContainerFactory()
+
+   .. attribute:: grok.name(u'waeup.CertificateContainer')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`CertificateContainer`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mycertificatecontainer = createObject(u'waeup.CertificateContainer')
+     >>> mycertificatecontainer
+     <waeup.sirp.university.certificatecontainer.CertificateContainer 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.certificatecontainer import (
+     ...   CertificateContainerFactory)
+     >>> verifyClass(IFactory, CertificateContainerFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> certificatecontainer_factory = CertificateContainerFactory()
+     >>> certificatecontainer_factory.getInterfaces()
+     <implementedBy waeup.sirp.university.certificatecontainer.CertificateContainer>
+
+
+Examples
+========
+
+Getting a certificate container
+-------------------------------
+
+We can easily create `CertificateContainers`:
+
+    >>> from waeup.sirp.university.certificatecontainer import CertificateContainer
+    >>> mycontainer = CertificateContainer()
+
+
+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.CertificateContainer``. Now we can
+ask for an object by calling the appropriate factory:
+
+    >>> from zope.component import createObject
+    >>> createObject(u'waeup.CertificateContainer')
+    <waeup.sirp.university.certificatecontainer.CertificateContainer object at 0x...>
+
+This way we get a thing that implements ICertificateContainer without
+imports or similar.
+
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/course.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/course.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/course.py	(revision 6703)
@@ -0,0 +1,41 @@
+"""Courses.
+"""
+import grok
+from zope.interface import implementedBy
+from zope.component.interfaces import IFactory
+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)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/course.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/course.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/course.txt	(revision 6703)
@@ -0,0 +1,143 @@
+:mod:`waeup.sirp.university.course` -- Courses
+**********************************************
+
+.. module:: waeup.sirp.university.course
+
+Components that represent courses.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
+
+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/uli-studentpw/src/waeup/sirp/university/coursecontainer.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/coursecontainer.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/coursecontainer.py	(revision 6703)
@@ -0,0 +1,65 @@
+"""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 ICourseContainer, ICourse
+
+class CourseContainer(grok.Container):
+    """See interfaces for description.
+    """
+    grok.implements(ICourseContainer)
+    grok.require('waeup.manageUniversity')
+
+    def __setitem__(self, name, course):
+        """See corresponding docstring in certificatecontainer.py.
+        """
+        if not ICourse.providedBy(course):
+            raise TypeError('CourseContainers 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(CourseContainer, self).__setitem__(name, course)
+
+    def addCourse(self, course):
+        """See corresponding docstring in certificatecontainer.py.
+        """
+        self[getattr(course, 'code', None)] = course
+
+    def clear(self):
+        """See corresponding docstring and comments in certificatecontainer.py.
+        """
+        self._SampleContainer__data.clear()
+        del self.__dict__['_BTreeContainer__len']
+
+class CourseContainerFactory(grok.GlobalUtility):
+    """A factory for course containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.CourseContainer')
+    title = u"Create a new course container.",
+    description = u"This factory instantiates new course containers."
+
+    def __call__(self, *args, **kw):
+        return CourseContainer(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(CourseContainer)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/coursecontainer.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/coursecontainer.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/coursecontainer.txt	(revision 6703)
@@ -0,0 +1,147 @@
+:mod:`waeup.sirp.university.coursecontainer` -- Course containers
+*****************************************************************
+
+.. module:: waeup.sirp.university.coursecontainer
+
+Containers for courses.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
+
+
+Content Classes (models and containers)
+=======================================
+
+:class:`CourseContainer`
+------------------------
+
+.. class:: CourseContainer()
+
+   Create a course container instance.
+
+   .. attribute:: grok.implements(ICourseContainer)
+
+   .. attribute:: grok.require('waeup.manageUniversity')
+
+   :class:`CourseContainer` instances are instances of
+   :class:`grok.Container` that hold and manage instances of
+   :class:`waeup.sirp.university.course.Course`.
+
+     >>> from waeup.sirp.university.coursecontainer import CourseContainer
+     >>> mycontainer = CourseContainer()
+     >>> mycontainer
+     <waeup.sirp.university.coursecontainer.CourseContainer object at 0x...>
+
+   Course containers provide `ICourseContainer`:
+
+     >>> from waeup.sirp.university.interfaces import ICourseContainer
+     >>> ICourseContainer.providedBy(mycontainer)
+     True
+
+   We can be sure, that the full interface is supported by the
+   CourseContainer class:
+
+     >>> from zope.interface.verify import verifyClass
+     >>> verifyClass(ICourseContainer, CourseContainer)
+     True
+
+   As normal ``grok.Container``, :class:`CourseContainer` 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.coursecontainer import CourseContainer
+        >>> mycontainer = CourseContainer()
+        >>> mycontainer.addCourse('blah')
+        Traceback (most recent call last):
+        ...
+        TypeError: CourseContainers 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:`CourseContainerFactory`
+-------------------------------
+
+.. class:: CourseContainerFactory()
+
+   .. attribute:: grok.name(u'waeup.CourseContainer')
+
+   .. attribute:: grok.implements(IFactory)
+
+   A named utility to deliver new instances of :class:`CourseContainer`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mycoursecontainer = createObject(u'waeup.CourseContainer')
+     >>> mycoursecontainer
+     <waeup.sirp.university.coursecontainer.CourseContainer 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.coursecontainer import CourseContainerFactory
+     >>> verifyClass(IFactory, CourseContainerFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> coursecontainer_factory = CourseContainerFactory()
+     >>> coursecontainer_factory.getInterfaces()
+     <implementedBy waeup.sirp.university.coursecontainer.CourseContainer>
+
+
+Examples
+========
+
+Getting a course container
+--------------------------
+
+We can easily create `CourseContainers`:
+
+    >>> from waeup.sirp.university.coursecontainer import CourseContainer
+    >>> mycontainer = CourseContainer()
+
+
+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.CourseContainer``. Now we can ask for an object
+by calling the appropriate factory:
+
+    >>> from zope.component import createObject
+    >>> createObject(u'waeup.CourseContainer')
+    <waeup.sirp.university.coursecontainer.CourseContainer object at 0x...>
+
+This way we get a thing that implements ICourseContainer without
+imports or similar.
+
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/courserefs.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/courserefs.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/courserefs.txt	(revision 6703)
@@ -0,0 +1,118 @@
+Course referrers
+****************
+
+Courses are referred to by certificate courses.
+
+:Test-Layer: functional
+
+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/uli-studentpw/src/waeup/sirp/university/department.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/department.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/department.py	(revision 6703)
@@ -0,0 +1,67 @@
+"""University departments.
+"""
+import grok
+from zope.component import createObject
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.sirp.university.interfaces import IDepartment, IDepartmentAdd
+from waeup.sirp.university.vocabularies import inst_types
+
+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(cls):
+        return ['waeup.local.DepartmentOfficer', 'waeup.local.ClearanceOfficer']
+
+    # 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 = createObject(u'waeup.CourseContainer')
+        self.courses.__parent__ = self
+        self.courses.__name__ = 'courses'
+        self.certificates = createObject(u'waeup.CertificateContainer')
+        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):
+        return "%s %s (%s)" % (inst_types.getTerm(self.title_prefix).title, 
+                                                  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/uli-studentpw/src/waeup/sirp/university/department.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/department.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/department.txt	(revision 6703)
@@ -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.WAeUPSIRPUnitTestLayer
+
+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
+
+      (ICourseContainer instance) A container for courses.
+
+      Each department has a courses container:
+
+        >>> mydept.courses
+        <waeup.sirp.university.coursecontainer.CourseContainer object at 0x...>
+
+
+   .. attribute:: certificates
+
+      (ICertificateContainer instance) A container for certificates.
+
+      Each department has a certificate container, that holds the
+      descrtiptions of certificates the department has:
+
+        >>> mydept.certificates
+        <waeup.sirp...certificatecontainer.CertificateContainer 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/uli-studentpw/src/waeup/sirp/university/faculty.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/faculty.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/faculty.py	(revision 6703)
@@ -0,0 +1,65 @@
+"""Faculty components.
+"""
+
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.sirp.university.interfaces import (
+    IFaculty, IFacultyAdd, IDepartment)
+from waeup.sirp.university.vocabularies import inst_types
+
+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(cls):
+        return ['waeup.local.DepartmentOfficer', 'waeup.local.ClearanceOfficer']
+
+    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 clear(self):
+        """Remove all departments from faculty.
+        """
+        keys = self.keys()
+        for key in keys:
+            del self[key]
+
+    def longtitle(self):
+        try:
+            result = "%s %s (%s)" % (
+                inst_types.getTerm(
+                    self.title_prefix).title,
+                self.title, self.code)
+        except:
+            result = "%s (%s)" % (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/uli-studentpw/src/waeup/sirp/university/faculty.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/faculty.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/faculty.txt	(revision 6703)
@@ -0,0 +1,207 @@
+:mod:`waeup.sirp.university.faculty` -- Faculties
+*************************************************
+
+.. module:: waeup.sirp.university.faculty
+
+Components that represent university faculties.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
+
+
+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...>)]
+
+   .. method:: clear()
+
+      Remove all departments from this faculty:
+
+        >>> myfac.clear()
+        >>> list(myfac.items())
+        []
+
+   .. 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/uli-studentpw/src/waeup/sirp/university/facultycontainer.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/facultycontainer.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/facultycontainer.py	(revision 6703)
@@ -0,0 +1,55 @@
+import grok
+from zope.component.interfaces import IFactory
+from zope.interface import implementedBy
+from waeup.sirp.interfaces import IWAeUPSIRPPluggable
+from waeup.sirp.university.interfaces import IFacultyContainer, IFaculty
+
+class FacultyContainer(grok.Container):
+    """See interfaces for description.
+    """
+    grok.implements(IFacultyContainer)
+    grok.require('waeup.manageUniversity')
+
+    def addFaculty(self, faculty):
+        if not IFaculty.providedBy(faculty):
+            raise TypeError('FacultyContainers contain only IFaculty instances')
+        self[faculty.code] = faculty
+        return
+
+    def clear(self):
+        keys = self.keys()
+        for key in keys:
+            del self[key]
+
+class FacultyContainerFactory(grok.GlobalUtility):
+    """A factory for faculty containers.
+    """
+    grok.implements(IFactory)
+    grok.name(u'waeup.FacultyContainer')
+    title = u"Create a new faculty container.",
+    description = u"This factory instantiates new faculty containers."
+
+    def __call__(self, *args, **kw):
+        return FacultyContainer(*args, **kw)
+
+    def getInterfaces(self):
+        return implementedBy(FacultyContainer)
+
+class AcademicsPlugin(grok.GlobalUtility):
+    """A plugin that creates a faculty container inside a university.
+    """
+    grok.implements(IWAeUPSIRPPluggable)
+    grok.name('faculties')
+
+    def setup(self, site, name, logger):
+        if 'faculties' in site.keys():
+            logger.warn('Could not create faculty container in WAeUP SIRP.')
+            return
+        site['faculties'] = FacultyContainer()
+        logger.info('Created faculty container for University')
+        return
+
+    def update(self, site, name, logger):
+        if not 'faculties' in site.keys():
+            self.setup(site, name, logger)
+
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/facultycontainer.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/facultycontainer.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/facultycontainer.txt	(revision 6703)
@@ -0,0 +1,186 @@
+:mod:`waeup.sirp.university.facultycontainer` -- Faculty Containers
+*******************************************************************
+
+.. module:: waeup.sirp.university.facultycontainer
+
+Components that represent faculty containers.
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
+
+
+Content Classes (models and containers)
+=======================================
+
+:class:`FacultyContainer`
+-------------------------
+
+.. class:: FacultyContainer()
+
+   Create a faculty container:
+
+     >>> from waeup.sirp.university.facultycontainer import FacultyContainer
+     >>> mycontainer = FacultyContainer()
+     >>> mycontainer
+     <waeup.sirp.university.facultycontainer.FacultyContainer object at 0x...>
+
+   Another way to create :class:`FacultyContainer` instances is by asking
+   for a factory called ``waeup.FacultyContainer``. This way we can create a
+   faculty without importing a class:
+
+     >>> from zope.component import createObject
+     >>> mycontainer = createObject(u'waeup.FacultyContainer')
+     >>> mycontainer
+     <waeup.sirp.university.facultycontainer.FacultyContainer object at 0x...>
+
+   :class:`FacultyContainer` instances have the attributes required by the
+   `IFacultyContainer` interface:
+
+     >>> from waeup.sirp.university.interfaces import IFacultyContainer
+     >>> IFacultyContainer.providedBy(mycontainer)
+     True
+
+     >>> from zope.interface.verify import verifyObject
+     >>> verifyObject(IFacultyContainer, 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: FacultyContainers 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...>)]
+
+   .. method:: clear()
+
+      Remove all departments from this faculty:
+
+        >>> mycontainer.clear()
+        >>> list(mycontainer.items())
+        []
+
+
+Utilities
+=========
+
+:class:`AcademicsPlugin`
+------------------------
+
+.. class:: AcademicsPlugin()
+
+   .. attribute:: grok.implements(IWAeUPSIRPPluggable)
+
+   This plugin component tells under which name (``faculties``) an
+   instance of academics stuff should be created in plain WAeUP SIRP
+   instances. It also tells the factory name for FacultyContainer
+   instances, which serve as root object for academics stuff in WAeUP
+   SIRP apps.
+
+
+:class:`FacultyContainerFactory`
+--------------------------------
+
+.. class:: FacultyContainerFactory()
+
+   .. attribute:: grok.name(u'waeup.FacultyContainer')
+
+   .. attribute:: grok.implements(IFactoryContainer)
+
+   A named utility to deliver new instances of :class:`FacultyContainer`
+   without the need to import the implementation before:
+
+     >>> from zope.component import createObject
+     >>> mycontainer = createObject(u'waeup.FacultyContainer')
+     >>> mycontainer
+     <waeup.sirp.university.facultycontainer.FacultyContainer 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.facultycontainer import (
+     ...   FacultyContainerFactory)
+     >>> verifyClass(IFactory, FacultyContainerFactory)
+     True
+
+   This means also, that we can get the interfaces of the created
+   object from the factory:
+
+     >>> fac_container_factory = FacultyContainerFactory()
+     >>> fac_container_factory.getInterfaces()
+     <implementedBy waeup.sirp.university.facultycontainer.FacultyContainer>
+
+Examples
+========
+
+We can easily create `FacultyContainers`:
+
+    >>> from waeup.sirp.university.facultycontainer import FacultyContainer
+    >>> mycontainer = FacultyContainer()
+
+Faculty containers provide `IFacultyContainer`:
+
+    >>> from waeup.sirp.university.interfaces import IFacultyContainer
+    >>> IFacultyContainer.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.facultycontainer``:
+
+    >>> from zope.component import createObject
+    >>> createObject(u'waeup.FacultyContainer')
+    <waeup.sirp.university.facultycontainer.FacultyContainer object at 0x...>
+
+This way we get a thing that implements IFacultyContainer without
+imports or similar.
+
+We can be sure, that the full interface is supported by the
+FacultyContainer class::
+
+    >>> from zope.interface.verify import verifyClass
+    >>> verifyClass(IFacultyContainer, FacultyContainer)
+    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: FacultyContainers 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/uli-studentpw/src/waeup/sirp/university/interfaces.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/interfaces.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/interfaces.py	(revision 6703)
@@ -0,0 +1,287 @@
+"""Interfaces of academics specific objects.
+"""
+from zope import schema
+from zope.interface import Attribute
+from waeup.sirp.interfaces import (IWAeUPObject, IWAeUPContainer)
+
+from waeup.sirp.university.vocabularies import (
+    course_levels,
+    semester,
+    application_categories,
+    study_mode,
+    inst_types,
+    CourseSource,
+    )
+
+
+class IFaculty(IWAeUPContainer):
+    """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',
+        vocabulary = inst_types,
+        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 IFacultyContainer(IWAeUPContainer):
+    """A container for faculties.
+    """
+    def addFaculty(faculty):
+        """Add an IFactulty object.
+
+        """
+class IDepartment(IWAeUPObject):
+    """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',
+        vocabulary = inst_types,
+        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 ICourseContainer(IWAeUPContainer):
+    """A container for faculties.
+    """
+    def addCourse(course):
+        """Add an ICourse object.
+
+        Returns the key, under which the object was stored.
+        """
+
+class ICourse(IWAeUPObject):
+    """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 = 0,
+        vocabulary = semester,
+        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(IWAeUPObject):
+    """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',
+        vocabulary = study_mode,
+        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',
+        vocabulary = application_categories,
+        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 ICertificateContainer(IWAeUPContainer):
+    """A container for certificates.
+    """
+    def addCertificate(certificate):
+        """Add an ICertificate object.
+
+        Returns the key, under which the object was stored.
+        """
+
+class ICertificateCourse(IWAeUPObject):
+    """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,
+        )
+
+    core_or_elective = 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/uli-studentpw/src/waeup/sirp/university/tests/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/__init__.py	(revision 6703)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_batching.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_batching.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_batching.py	(revision 6703)
@@ -0,0 +1,310 @@
+# 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/uli-studentpw/src/waeup/sirp/university/tests/test_certificatecontainer.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_certificatecontainer.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_certificatecontainer.py	(revision 6703)
@@ -0,0 +1,143 @@
+##
+## test_certificatecontainer.py
+## Login : <uli@pu.smp.net>
+## Started on  Sun May 29 02:21:16 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
+##
+"""
+Tests for certificate containers and related components.
+"""
+import shutil
+import tempfile
+import unittest
+from zope.app.testing.functional import FunctionalTestCase
+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
+from waeup.sirp.university.certificate import Certificate
+from waeup.sirp.university.certificatecontainer import CertificateContainer
+from waeup.sirp.university.interfaces import ICertificateContainer
+
+
+class CertificateContainerTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(CertificateContainerTests, 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(CertificateContainerTests, self).tearDown()
+        clearSite()
+        shutil.rmtree(self.dc_root)
+
+    def test_create(self):
+        # we can create instances
+        container = CertificateContainer()
+        result = isinstance(container, CertificateContainer)
+        assert result is True
+
+    def test_provides_icertificatecontainer(self):
+        # instances tell they provide their main interface
+        container = CertificateContainer()
+        result = ICertificateContainer.providedBy(container)
+        assert result is True
+
+    def test_interfaces(self):
+        # class and instances comply with their main interface
+        container = CertificateContainer()
+        result1 = verifyClass(ICertificateContainer, CertificateContainer)
+        result2 = verifyObject(ICertificateContainer, container)
+
+    def test_add_certificate_non_icertificate(self):
+        # we cannot add certificates that do not implement ICertificate.
+        container = CertificateContainer()
+        self.assertRaises(
+            TypeError, container.addCertificate, 'some_string')
+
+    def test_add_certificate_no_duplicate_entry(self):
+        # we cannot add certificates that are registered already.
+        container = CertificateContainer()
+        # 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 = CertificateContainer()
+        container2 = CertificateContainer()
+        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 = CertificateContainer()
+        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 = CertificateContainer()
+        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(CertificateContainerTests),
+        doctestsuite_for_module(
+                'waeup.sirp.university.certificatecontainer'),
+        ))
+    return suite
+
+if __name__=='__main__':
+    unittest.main(defaultTest='test_suite')
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_coursecontainer.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_coursecontainer.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_coursecontainer.py	(revision 6703)
@@ -0,0 +1,21 @@
+# Test course containers
+
+import unittest
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.sirp.university.interfaces import ICourseContainer
+from waeup.sirp.university import CourseContainer, Course
+
+class CourseContainerTests(unittest.TestCase):
+
+    def test_ifaces(self):
+        container = CourseContainer()
+        self.assertTrue(verifyClass(ICourseContainer, CourseContainer))
+        self.assertTrue(verifyObject(ICourseContainer, container))
+
+    def test_setitem_name_ne_code(self):
+        # If we add a course under a wrong name that will give an error
+        container = CourseContainer()
+        course = Course(code='MYCOURSE')
+        self.assertRaises(
+            ValueError,
+            container.__setitem__, 'NOTMYCOURSE', course)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_faculty.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_faculty.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_faculty.py	(revision 6703)
@@ -0,0 +1,30 @@
+# Test faculties
+import tempfile
+import shutil
+import unittest
+from zope.interface.verify import verifyObject, verifyClass
+from waeup.sirp.university import Faculty
+from waeup.sirp.university.interfaces import IFaculty
+
+class FacultyTests(unittest.TestCase):
+
+    def test_ifaces(self):
+        # Make sure we implement the promised interfaces
+        fac = Faculty()
+        verifyClass(IFaculty, Faculty)
+        verifyObject(IFaculty, fac)
+        return
+
+    def test_longtitle(self):
+        # Make sure we get a longtitle
+        fac = Faculty()
+        self.assertEqual(fac.longtitle(),
+                         u'Faculty of Unnamed Faculty (NA)')
+
+    def test_longtitle_no_terms(self):
+        # Make sure we get a longtitle also if no term for the title
+        # prefix is available
+        fac = Faculty()
+        fac.title_prefix = 'Studio for' # Not a valid prefix
+        self.assertEqual(fac.longtitle(),
+                         u'Unnamed Faculty (NA)')
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_facultycontainer.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_facultycontainer.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_facultycontainer.py	(revision 6703)
@@ -0,0 +1,96 @@
+# Tests for FacultyContainer.
+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 IFacultyContainer
+from waeup.sirp.university import FacultyContainer
+from waeup.sirp.university.facultycontainer 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 FacultyContainerTests(unittest.TestCase):
+
+    def test_ifaces(self):
+        container = FacultyContainer()
+        self.assertTrue(verifyClass(IFacultyContainer, FacultyContainer))
+        self.assertTrue(verifyObject(IFacultyContainer, 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'], FacultyContainer))
+        self.assertEqual(
+            self._logger_factory.get_messages(),
+            'Created faculty container for University\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 faculty container in WAeUP 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'], FacultyContainer))
+        self.assertEqual(
+            self._logger_factory.get_messages(),
+            'Created faculty container for University\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/uli-studentpw/src/waeup/sirp/university/tests/test_university.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_university.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/tests/test_university.py	(revision 6703)
@@ -0,0 +1,12 @@
+# Test the public API part of the university subpackage
+import unittest
+from waeup.sirp.university import *
+
+class UniversitySubpackageTests(unittest.TestCase):
+
+    def test_public_api(self):
+        names = globals().keys()
+        self.assertTrue('Course' in names)
+        self.assertTrue('CourseContainer' in names)
+        self.assertTrue('Faculty' in names)
+        self.assertTrue('FacultyContainer' in names)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/vocabularies.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/vocabularies.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/university/vocabularies.py	(revision 6703)
@@ -0,0 +1,72 @@
+"""Vocabularies and sources for the academics section.
+"""
+from zc.sourcefactory.basic import BasicSourceFactory
+from zope.catalog.interfaces import ICatalog
+from zope.component import getUtility
+from waeup.sirp.interfaces import SimpleWAeUPVocabulary
+
+inst_types = SimpleWAeUPVocabulary(
+    ('Faculty of','faculty'),
+    ('Department of','department'),
+    ('School of','school'),
+    ('School for','school_for'),
+    ('Institute of','institute'),
+    ('Office for','office'),
+    ('Centre for','centre'),
+    ('College','college'),
+    )
+
+course_levels = SimpleWAeUPVocabulary(
+    ('Pre-Studies',0),
+    ('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),
+    )
+
+semester = SimpleWAeUPVocabulary(
+    ('N/A', 0),
+    ('First Semester', 1),
+    ('Second Semester', 2),
+    ('Combined', 3)
+    )
+
+application_categories = SimpleWAeUPVocabulary(
+    ('--',''),
+    ('PUME, PDE, PCE, PRENCE','basic'),
+    ('Part-Time, Diploma, Certificate','cest'),
+    ('Sandwich','sandwich'),
+    ('Postgraduate','pg'),
+    )
+
+study_mode = SimpleWAeUPVocabulary(
+    ('UME Full Time','ume_ft'),
+    ('Direct Entry Full Time','de_ft'),
+    ('Diploma Full Time','dp_ft'),
+    ('Diploma Part Time','dp_pt'),
+    ('Undergraduate Full Time','ug_ft'),
+    ('Undergraduate Part Time','ug_pt'),
+    ('Postgraduate Full Time','pg_ft'),
+    ('Postgraduate Part Time','pg_pt'),
+    )
+
+
+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/uli-studentpw/src/waeup/sirp/users.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/users.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/users.py	(revision 6703)
@@ -0,0 +1,75 @@
+"""Users (principals) for the WAeUP portal.
+"""
+import grok
+from zope.event import notify
+from zope.interface import Interface
+from zope.securitypolicy.interfaces import IPrincipalRoleMap
+from waeup.sirp.authentication import Account
+from waeup.sirp.interfaces import IUserContainer, ILocalRoleSetEvent
+
+class UserContainer(grok.Container):
+    """A container for principals.
+
+    See interfaces.py and users.txt for extensive description.
+    """
+    grok.implements(IUserContainer)
+    grok.require('waeup.manageUsers')
+
+    def addUser(self, name, password, title=None, description=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, password, title, description, roles)
+
+    def addAccount(self, account):
+        """Add the account passed.
+        """
+        self[account.name] = account
+
+    def delUser(self, name):
+        """Delete user, if an account with the given name exists.
+
+        Do not complain, if the name does not exist.
+        """
+        if name in self.keys():
+            del self[name]
+
+class 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(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_removal(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/uli-studentpw/src/waeup/sirp/users.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/users.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/users.txt	(revision 6703)
@@ -0,0 +1,68 @@
+User for the WAeUP portal
+*************************
+
+.. :doctest:
+.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
+
+Before we can start, we need some password managers available:
+
+    >>> from zope.app.authentication.placelesssetup import (
+    ...   PlacelessSetup)
+    >>> PlacelessSetup().setUp()
+
+User Containers
+===============
+
+We can create a user container which will hold the useraccounts for
+us:
+
+    >>> from waeup.sirp.users import UserContainer
+    >>> myusers = UserContainer()
+
+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
+    'bob'
+
+As we did not give a title nor description, the name was taken instead
+for these values. 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/uli-studentpw/src/waeup/sirp/utils/api.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/api.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/api.txt	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/utils/batching.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/batching.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/batching.py	(revision 6703)
@@ -0,0 +1,310 @@
+"""WAeUP 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...
+        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():
+            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':
+                # Ignore non-location fields when removing...
+                field = '--IGNORE--'
+            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():
+            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 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.keys()
+        failed_headers.append('--ERRORS--')
+        failed_writer = csv.DictWriter(open(failed_path, 'wb'),
+                                       failed_headers)
+        first_row = mapping.items()
+        first_row.append(("--ERRORS--", "--ERRORS--"),)
+        failed_writer.writerow(dict(first_row))
+
+        finished_path = os.path.join(temp_dir, "%s.finished%s" % (base, ext))
+        finished_headers = [x for x in 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()
+        converter = IObjectConverter(self.iface)
+        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 =  converter.fromStringDict(
+                string_row, self.factory_name)
+            if errs or inv_errs:
+                num_warns += 1
+                conv_warnings = self.stringFromErrs(errs, inv_errs)
+                self.writeFailedRow(
+                    failed_writer, raw_row, conv_warnings)
+                continue
+            row.update(conv_dict)
+
+            if mode == 'create':
+                if not self.parentsExist(row, site):
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, raw_row,
+                        "Not all parents do exist yet. Skipping")
+                    continue
+                if self.entryExists(row, site):
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, raw_row,
+                        "This object already exists in the same container. Skipping.")
+                    continue
+                obj = self.callFactory()
+                for key, value in row.items():
+                    setattr(obj, key, value)
+                try:
+                    self.addEntry(obj, row, site)
+                except KeyError, error:
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, raw_row,
+                        "%s Skipping." % error.message)
+                    continue
+            elif mode == 'remove':
+                if not self.entryExists(row, site):
+                    num_warns += 1
+                    self.writeFailedRow(
+                        failed_writer, raw_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, raw_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/uli-studentpw/src/waeup/sirp/utils/batching.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/batching.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/batching.txt	(revision 6703)
@@ -0,0 +1,573 @@
+:mod:`waeup.sirp.utils.batching` -- Batch processing
+****************************************************
+
+Batch processing is much more than pure data import.
+
+:test-layer: functional
+
+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 will be populated correctly 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()
+    --IGNORE--,name,--IGNORE--,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.
+
+The first ignored column ('owner') provides different contents than
+the second one ('taxpayer').
+
+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/uli-studentpw/src/waeup/sirp/utils/converters.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/converters.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/converters.py	(revision 6703)
@@ -0,0 +1,173 @@
+"""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
+        self.default_form_fields = form.Fields(iface)
+        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/uli-studentpw/src/waeup/sirp/utils/converters.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/converters.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/converters.txt	(revision 6703)
@@ -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.WAeUPSIRPUnitTestLayer
+
+
+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 SimpleWAeUPVocabulary
+    >>> field = Choice(
+    ...   title = u'Favourite Dish',
+    ...   default = 0,
+    ...   vocabulary = SimpleWAeUPVocabulary(
+    ...        ('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/uli-studentpw/src/waeup/sirp/utils/helpers.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/helpers.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/helpers.py	(revision 6703)
@@ -0,0 +1,404 @@
+"""General helper functions for WAeUP.
+"""
+import os
+import re
+import sys
+import shutil
+import grok
+from cStringIO import StringIO
+from docutils.core import publish_string
+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
+
+BUFSIZE = 8 * 1024
+
+def removeFileOrDirectory(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 copyFileSystemTree(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:
+                removeFileOrDirectory(itemdst)
+            else:
+                not_copied.append(item)
+                continue
+
+        if os.path.isdir(itemsrc):
+            shutil.copytree(itemsrc, itemdst)
+        else:
+            shutil.copy2(itemsrc, itemdst)
+        if del_old:
+            removeFileOrDirectory(itemsrc)
+    return not_copied
+
+
+def getInnerHTMLPart(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>'
+       >>> getInnerHTMLPart(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>'
+       >>> getInnerHTMLPart(doc)
+       'My Body'
+
+    If none of these is included, return what we got:
+
+       >>> doc = '<html>without body nor form</html>'
+       >>> getInnerHTMLPart(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 = getInnerHTMLPart(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
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/helpers.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/helpers.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/helpers.txt	(revision 6703)
@@ -0,0 +1,221 @@
+:mod:`waeup.sirp.utils.helpers` -- Helpers for the WAeUP SRP
+************************************************************
+
+.. module:: waeup.sirp.utils.helpers
+
+Helper functions for the WAeUP SRP.
+
+.. :doctest:
+
+:func:`removeFileOrDirectory`
+=============================
+
+.. function:: removeFileOrDirectory(path)
+
+   Removes a file or directory given by a path. We can remove files:
+
+     >>> import os
+     >>> from waeup.sirp.utils.helpers import removeFileOrDirectory
+     >>> open('blah', 'wb').write('nonsense')
+     >>> 'blah' in os.listdir('.')
+     True
+
+     >>> removeFileOrDirectory('blah')
+     >>> 'blah' in os.listdir('.')
+     False
+
+   We can remove directories:
+
+     >>> os.mkdir('blah')
+     >>> 'blah' in os.listdir('.')
+     True
+
+     >>> removeFileOrDirectory('blah')
+     >>> 'blah' in os.listdir('.')
+     False
+
+
+:func:`copyFileSystemTree`
+==========================
+
+.. function:: copyFileSystemTree(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 copyFileSystemTree
+     >>> result = copyFileSystemTree('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 = copyFileSystemTree('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 = copyFileSystemTree('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 = copyFileSystemTree('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 = copyFileSystemTree('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 = copyFileSystemTree('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:
+
+    >>> removeFileOrDirectory('src')
+    >>> removeFileOrDirectory('dst')
+
+
+:func:`getInnerHTMLPart()`
+==========================
+
+.. function:: getInnerHTMLPart(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 getInnerHTMLPart
+     >>> print getInnerHTMLPart("""<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 getInnerHTMLPart("""<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 getInnerHTMLPart("""<div>
+     ...  <div>Some content</div>
+     ... </div>
+     ... """)
+     <div>
+      <div>Some content</div>
+     </div>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/importexport.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/importexport.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/importexport.py	(revision 6703)
@@ -0,0 +1,84 @@
+import csv
+import grok
+import cPickle
+import zope.xmlpickle
+from cStringIO import StringIO
+from zope.interface import Interface
+from waeup.sirp.interfaces import (IWAeUPObject, IWAeUPExporter,
+                                   IWAeUPXMLExporter, IWAeUPXMLImporter)
+
+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 WAeUP object as pickle.
+
+    This all-purpose exporter exports attributes defined in schemata
+    and contained objects (if the exported thing is a container).
+    """
+    grok.context(IWAeUPObject)
+    grok.provides(IWAeUPExporter)
+
+    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 WAeUP object as XML.
+
+    This all-purpose exporter exports XML representations of pickable
+    objects.
+    """
+    grok.context(Interface)
+    grok.provides(IWAeUPXMLExporter)
+
+    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 WAeUP object from XML.
+    """
+    grok.context(Interface)
+    grok.provides(IWAeUPXMLImporter)
+
+    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/uli-studentpw/src/waeup/sirp/utils/importexport.txt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/importexport.txt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/importexport.txt	(revision 6703)
@@ -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.WAeUPSIRPUnitTestLayer
+
+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 IWAeUPXMLExporter
+    >>> exporter = IWAeUPXMLExporter(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 IWAeUPXMLImporter
+    >>> importer = IWAeUPXMLImporter(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/uli-studentpw/src/waeup/sirp/utils/logger.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/logger.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/logger.py	(revision 6703)
@@ -0,0 +1,378 @@
+##
+## logging.py
+## Login : <uli@pu.smp.net>
+## Started on  Mon Jun 13 01:25:07 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
+##
+"""
+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
+
+#: 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 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)
+        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)
+        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 - %(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 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.
+        """
+
+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_move(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
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/overrides.zcml
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/overrides.zcml	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/overrides.zcml	(revision 6703)
@@ -0,0 +1,11 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+
+  <adapter
+      for="zope.schema.interfaces.IBool
+           zope.publisher.interfaces.browser.IBrowserRequest"
+      provides="zope.formlib.interfaces.ISimpleInputWidget"
+      factory=".converters.ExtendedCheckBoxWidget"
+      permission="zope.Public"
+      />
+
+</configure>
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/tests/test_batching.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/tests/test_batching.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/tests/test_batching.py	(revision 6703)
@@ -0,0 +1,241 @@
+##
+## test_batching.py
+## Login : <uli@pu.smp.net>
+## Started on  Fri Jun  3 00:28:43 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
+##
+import doctest
+import logging
+import os
+import shutil
+import tempfile
+import unittest
+from zope import schema
+from zope.app.testing.functional import FunctionalTestCase
+from zope.component import provideUtility
+from zope.component.factory import Factory
+from zope.component.hooks import setSite, 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, doctestsuite_for_module
+from waeup.sirp.utils.helpers import attrs_to_fields
+
+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/uli-studentpw/src/waeup/sirp/utils/tests/test_converters.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/tests/test_converters.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/tests/test_converters.py	(revision 6703)
@@ -0,0 +1,353 @@
+##
+## test_converters.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue May 31 08:34:44 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
+##
+"""
+Tests for converterts.
+"""
+import datetime
+import os
+import shutil
+import tempfile
+import unittest
+from zope import schema
+from zope.app.testing.functional import FunctionalTestCase
+from zope.component import provideUtility
+from zope.component.factory import Factory
+from zope.component.hooks import setSite, clearSite
+from zope.component.interfaces import IFactory
+from zope.formlib import form
+from zope.interface import (
+    Interface, implements, invariant, Invalid, implementedBy)
+from zope.interface.verify import verifyClass, verifyObject
+
+from waeup.sirp.app import University
+from waeup.sirp.testing import FunctionalLayer, doctestsuite_for_module
+from waeup.sirp.utils.converters import IObjectConverter
+from waeup.sirp.utils.helpers import attrs_to_fields
+from waeup.sirp.interfaces import SimpleWAeUPVocabulary
+
+colors = SimpleWAeUPVocabulary(
+    ('Red', u'red'),
+    ('Green', u'green'),
+    ('Blue', u'blue'),
+    )
+car_nums = SimpleWAeUPVocabulary(
+    ('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
+        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)
+
+        # The preferred 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)
+
+        # '08' is not interpreted as octal number
+        err, inv_err, data = converter.fromStringDict(
+            dict(birthday='1945-12-08'), contact)
+        assert data['birthday'] == datetime.date(1945, 12, 8)
+        return
+
+    def test_date_invalid(self):
+        contact = Contact()
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            dict(birthday='not-a-date'), contact)
+        self.assertEqual(err, [('birthday', u'Invalid datetime data')])
+
+    def test_inject_formfields_select(self):
+        # We can use our own formfields and select only a subset of fields
+        contact = Contact()
+        converter = IObjectConverter(IContact) # a converter to IContact
+        input_data = dict(name='Bruno', age='99', vip='on')
+        err, inv_err, data = converter.fromStringDict(
+            input_data, contact, form_fields=form_fields_select)
+        self.assertEqual(data['name'], 'Bruno')
+        assert 'age' not in data.keys()
+        assert data['vip'] is True
+        return
+
+    def test_inject_formfields_omit(self):
+        # We can use our own formfields and omit some fields
+        contact = Contact()
+        converter = IObjectConverter(IContact) # a converter to IContact
+        input_data = dict(name='Bruno', age='99', vip='on')
+        err, inv_err, data = converter.fromStringDict(
+            input_data, contact, form_fields=form_fields_omit)
+        self.assertEqual(data['age'], 99)
+        assert 'name' not in data.keys()
+        assert 'vip' not in data.keys()
+        return
+
+    def test_factory(self):
+        # We can use factories to convert values
+        converter = IObjectConverter(IContact) # a converter to IContact
+        # pass string ``contact`` instead of a real object
+        err, inv_err, data = converter.fromStringDict(
+            dict(name='Gabi', age='23'), 'contact')
+        self.assertEqual(data['age'], 23)
+        self.assertEqual(data['name'], u'Gabi')
+        return
+
+    def test_choice_vocab(self):
+        # We can handle vocabularies
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            dict(fav_color='blue'), 'contact')
+        assert data['fav_color'] == u'blue'
+        assert isinstance(data['fav_color'], unicode)
+        return
+
+    def test_choice_vocab_invalid_value(self):
+        # We can handle vocabularies
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            dict(fav_color='magenta'), 'contact')
+        self.assertEqual(err, [('fav_color', u'Invalid value')])
+        assert 'fav_color' not in data.keys()
+        return
+
+    def test_non_string_choice(self):
+        # We can handle vocabs with non-string values
+        converter = IObjectConverter(IContact) # a converter to IContact
+        err, inv_err, data = converter.fromStringDict(
+            dict(num_cars='1'), 'contact')
+        assert data['num_cars'] == 1
+        return
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    for testcase in [
+        ConverterTests,
+        ]:
+        suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+                testcase
+                )
+        )
+    return suite
+
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/tests/test_helpers.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/tests/test_helpers.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/tests/test_helpers.py	(revision 6703)
@@ -0,0 +1,246 @@
+# -*- coding: utf-8 -*-
+
+##
+## test_helpers.py
+## Login : <uli@pu.smp.net>
+## Started on  Sat Feb 12 14:39:42 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
+##
+import os
+import shutil
+import tempfile
+import unittest
+import doctest
+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.removeFileOrDirectory(self.non_file)
+        self.assertTrue(result is None)
+        return
+
+    def test_handle_dir(self):
+        helpers.removeFileOrDirectory(self.dirpath)
+        self.assertFalse(
+            os.path.exists(self.dirpath)
+            )
+        return
+
+    def test_handle_file(self):
+        helpers.removeFileOrDirectory(self.filepath)
+        self.assertFalse(
+            os.path.exists(self.filepath)
+            )
+        return
+
+class CopyFileSystemTreeTestCase(unittest.TestCase):
+    # Test edge cases of copyFileSystemTree().
+    #
+    # 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.copyFileSystemTree(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.copyFileSystemTree,
+            self.not_existing_dir,
+            self.existing_dst
+            )
+        return
+
+    def test_dest_not_existing(self):
+        self.assertRaises(
+            ValueError,
+            helpers.copyFileSystemTree,
+            self.existing_src,
+            self.not_existing_dir
+            )
+        return
+
+    def test_src_not_a_dir(self):
+        self.assertRaises(
+            ValueError,
+            helpers.copyFileSystemTree,
+            self.filepath,
+            self.existing_dst
+            )
+        return
+
+    def test_dst_not_a_dir(self):
+        self.assertRaises(
+            ValueError,
+            helpers.copyFileSystemTree,
+            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
+
+def test_suite():
+    suite = unittest.TestSuite()
+    # Register local test cases...
+    for testcase in [
+        ReST2HTMLTestCase,
+        FactoryBaseTestCase,
+        CopyFileSystemTreeTestCase,
+        RemoveFileOrDirectoryTestCase,
+        CurrentPrincipalTestCase,
+        CmpFilesTestCase,
+        ]:
+        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/uli-studentpw/src/waeup/sirp/utils/tests/test_logger.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/tests/test_logger.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/utils/tests/test_logger.py	(revision 6703)
@@ -0,0 +1,403 @@
+# 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, get_all_loggers,
+    remove_new_loggers, remove_logger)
+
+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()
+        self.old_loggers = get_all_loggers()
+        # 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()
+        remove_new_loggers(self.old_loggers)
+        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)
+        self.old_loggers = get_all_loggers()
+        return
+
+    def tearDown(self):
+        super(LoggersFunctionalTests, self).tearDown()
+        remove_new_loggers(self.old_loggers)
+        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 - msg from do_something\n'))
+        return
+
+class LoggerCollectorTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(LoggerCollectorTests, self).setUp()
+        self.old_loggers = get_all_loggers()
+        # 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()
+        remove_new_loggers(self.old_loggers)
+        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()
+        remove_logger('waeup.sirp.app') # In case it lingers around
+        self.old_loggers = get_all_loggers()
+        # 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()
+        remove_new_loggers(self.old_loggers)
+        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.
+        #return
+        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
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/__init__.py	(revision 6703)
@@ -0,0 +1,2 @@
+"""JS-driven widgets for WAeUP.
+"""
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/datewidget.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/datewidget.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/datewidget.py	(revision 6703)
@@ -0,0 +1,146 @@
+##
+## datewidget.py
+## Login : <uli@pu.smp.net>
+## Started on  Wed May 11 15:53:35 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
+##
+"""
+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
+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:
+                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 :var:`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 :var:`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)
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/interfaces.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/interfaces.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/interfaces.py	(revision 6703)
@@ -0,0 +1,6 @@
+##
+## interfaces.py
+"""Interfaces for widgets.
+"""
+from zope.interface import Interface
+
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/multilistwidget.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/multilistwidget.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/multilistwidget.pt	(revision 6703)
@@ -0,0 +1,38 @@
+<table border="0" class="multisequencewidget"
+  i18n:domain="zope">
+  <div tal:repeat="widget view/widgets"
+       tal:omit-tag="" >
+    <tr tal:condition="repeat/widget/start">
+      <th></th>
+      <th tal:repeat="subwidget widget/subwidgets">
+	<span tal:replace="subwidget/label">Column Label</span>
+      </th>
+    </tr>
+    <tr>
+      <td>
+	<input class="editcheck" type="checkbox"
+	       tal:attributes="
+                   name string:${view/name}.remove_${repeat/widget/index}"
+	       tal:condition="view/need_delete" />
+      </td>
+      <td tal:repeat="subwidget widget/subwidgets">
+	<span tal:define="error subwidget/error"
+	      tal:replace="structure error" tal:condition="error" />
+	<input tal:replace="structure subwidget" />
+      </td>
+    </tr>
+  </div>
+  <tr>
+    <td colspan="2">
+      <input type="submit" value="Remove selected items"
+	     tal:condition="view/need_delete"
+	     tal:attributes="name string:${view/name}.remove"
+	     i18n:attributes="value remove-selected-items" />
+      <input type="submit" value="Add foo"
+	     tal:condition="view/need_add"
+	     tal:attributes="name string:${view/name}.add;
+			     value view/addButtonLabel" />
+    </td>
+  </tr>
+</table>
+<input tal:replace="structure view/marker" />
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/objectwidget.pt
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/objectwidget.pt	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/objectwidget.pt	(revision 6703)
@@ -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/uli-studentpw/src/waeup/sirp/widgets/objectwidget.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/objectwidget.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/objectwidget.py	(revision 6703)
@@ -0,0 +1,77 @@
+##
+## resultswidget.py
+## Login : <uli@pu.smp.net>
+## Started on  Tue Jul 20 02:04:30 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
+##
+"""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 WAeUPObjectWidgetView(ObjectWidgetView):
+    template = ViewPageTemplateFile('objectwidget.pt')
+
+class WAeUPObjectWidget(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 WAeUPObjectWidgetView(self, request)
+
+class WAeUPObjectDisplayWidget(WAeUPObjectWidget):
+
+    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/uli-studentpw/src/waeup/sirp/widgets/restwidget.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/restwidget.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/restwidget.py	(revision 6703)
@@ -0,0 +1,38 @@
+##
+## restwidget.py
+## 
+## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public 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.formlib.textwidgets import TextWidget
+from zope.formlib.widget import renderElement, DisplayWidget
+from waeup.sirp.utils.helpers import ReST2HTML
+
+
+class ReSTDisplayWidget(DisplayWidget):
+    """Restructured Text widget.
+    """
+
+    def __call__(self):
+        if self._renderedValueSet():
+            value = self._data
+        else:
+            value = self.context.default
+        if value == self.context.missing_value:
+            return ""
+        return renderElement(u'div id="rest"',
+                             contents=ReST2HTML(value))
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/tests/__init__.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/tests/__init__.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/tests/__init__.py	(revision 6703)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/tests/test_datewidget.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/tests/test_datewidget.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/tests/test_datewidget.py	(revision 6703)
@@ -0,0 +1,141 @@
+##
+## test_datewidget.py
+## Login : <uli@pu.smp.net>
+## Started on  Thu May 12 02:17:06 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
+##
+"""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.failIf(self._widget.hasInput())
+        self._widget.request.form['field.foo'] = u''
+        self.failUnless(self._widget.hasInput())
+        self._widget.request.form['field.foo'] = u'2003-03-26'
+        self.failUnless(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.failIf(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/uli-studentpw/src/waeup/sirp/widgets/tests/test_objectwidget.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/tests/test_objectwidget.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/widgets/tests/test_objectwidget.py	(revision 6703)
@@ -0,0 +1,219 @@
+"""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 WAeUPObjectWidget as ObjectWidget
+from waeup.sirp.widgets.objectwidget import (
+    WAeUPObjectDisplayWidget 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.failUnless("email: <zope.formlib.interfaces.Mis" 
+                             in error_html)
+            self.failUnless("name: <zope.formlib.interfaces.Miss"
+                             in error_html)
+        else:
+            self.failUnless(
+                "email: MissingInputError(u'field.foo.email', u'', None)"
+                in error_html)
+            self.failUnless(
+                "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/uli-studentpw/src/waeup/sirp/workflow.py
===================================================================
--- /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/workflow.py	(revision 6703)
+++ /main/waeup.sirp/branches/uli-studentpw/src/waeup/sirp/workflow.py	(revision 6703)
@@ -0,0 +1,96 @@
+##
+## workflow.py
+## Login : <uli@pu.smp.net>
+## Started on  Sat Jun 11 19:52:12 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
+##
+"""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 IWAeUPWorkflowInfo
+
+class WAeUPWorkflow(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 WAeUPWorkflowInfo(WorkflowInfo):
+    """A workflow info that provides a convenience transition getter.
+    """
+
+    grok.provides(IWAeUPWorkflowInfo)
+
+    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)]
